Merge branch 'master' into 4009-external-users
This commit is contained in:
commit
59064aeeef
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"always-semicolon": true,
|
||||
"color-case": "lower",
|
||||
"block-indent": " ",
|
||||
"color-shorthand": true,
|
||||
"element-case": "lower",
|
||||
"space-before-colon": "",
|
||||
"space-after-colon": " ",
|
||||
"space-before-combinator": " ",
|
||||
"space-after-combinator": " ",
|
||||
"space-between-declarations": "\n",
|
||||
"space-before-opening-brace": " ",
|
||||
"space-after-opening-brace": "\n",
|
||||
"space-before-closing-brace": "\n",
|
||||
"unitless-zero": true
|
||||
}
|
|
@ -122,6 +122,14 @@ rubocop:
|
|||
- ruby
|
||||
- mysql
|
||||
|
||||
scss-lint:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake scss_lint
|
||||
tags:
|
||||
- ruby
|
||||
allow_failure: true
|
||||
|
||||
brakeman:
|
||||
stage: test
|
||||
script:
|
||||
|
@ -148,13 +156,14 @@ flay:
|
|||
|
||||
bundler:audit:
|
||||
stage: test
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- "bundle exec bundle-audit update"
|
||||
- "bundle exec bundle-audit check"
|
||||
- "bundle exec bundle-audit check --ignore OSVDB-115941"
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
allow_failure: true
|
||||
|
||||
# Ruby 2.2 jobs
|
||||
|
||||
|
@ -162,7 +171,7 @@ spec:feature:ruby22:
|
|||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
# Linter Documentation:
|
||||
# https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
|
||||
|
||||
scss_files: 'app/assets/stylesheets/**/*.scss'
|
||||
|
||||
exclude:
|
||||
- 'app/assets/stylesheets/pages/emojis.scss'
|
||||
|
||||
linters:
|
||||
BangFormat:
|
||||
enabled: false
|
||||
|
||||
BorderZero:
|
||||
enabled: false
|
||||
|
||||
ColorKeyword:
|
||||
enabled: false
|
||||
|
||||
ColorVariable:
|
||||
enabled: false
|
||||
|
||||
Comment:
|
||||
enabled: false
|
||||
|
||||
DeclarationOrder:
|
||||
enabled: false
|
||||
|
||||
# `scss-lint:disable` control comments should be preceded by a comment
|
||||
# explaining why these linters are being disabled for this file.
|
||||
# See https://github.com/brigade/scss-lint#disabling-linters-via-source for
|
||||
# more information.
|
||||
DisableLinterReason:
|
||||
enabled: true
|
||||
|
||||
DuplicateProperty:
|
||||
enabled: false
|
||||
|
||||
EmptyLineBetweenBlocks:
|
||||
enabled: false
|
||||
|
||||
EmptyRule:
|
||||
enabled: false
|
||||
|
||||
FinalNewline:
|
||||
enabled: false
|
||||
|
||||
# HEX colors should use three-character values where possible.
|
||||
HexLength:
|
||||
enabled: true
|
||||
|
||||
# HEX color values should use lower-case colors to differentiate between
|
||||
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
|
||||
HexNotation:
|
||||
enabled: true
|
||||
|
||||
IdSelector:
|
||||
enabled: false
|
||||
|
||||
ImportPath:
|
||||
enabled: false
|
||||
|
||||
ImportantRule:
|
||||
enabled: false
|
||||
|
||||
# Indentation should always be done in increments of 2 spaces.
|
||||
Indentation:
|
||||
enabled: true
|
||||
width: 2
|
||||
|
||||
LeadingZero:
|
||||
enabled: false
|
||||
|
||||
MergeableSelector:
|
||||
enabled: false
|
||||
|
||||
NameFormat:
|
||||
enabled: false
|
||||
|
||||
NestingDepth:
|
||||
enabled: false
|
||||
|
||||
PlaceholderInExtend:
|
||||
enabled: false
|
||||
|
||||
PropertySortOrder:
|
||||
enabled: false
|
||||
|
||||
PropertySpelling:
|
||||
enabled: false
|
||||
|
||||
PseudoElement:
|
||||
enabled: false
|
||||
|
||||
QualifyingElement:
|
||||
enabled: false
|
||||
|
||||
SelectorDepth:
|
||||
enabled: false
|
||||
|
||||
# Selectors should always use hyphenated-lowercase, rather than camelCase or
|
||||
# snake_case.
|
||||
SelectorFormat:
|
||||
enabled: true
|
||||
convention: hyphenated_lowercase
|
||||
|
||||
# Prefer the shortest shorthand form possible for properties that support it.
|
||||
Shorthand:
|
||||
enabled: true
|
||||
|
||||
# Each property should have its own line, except in the special case of
|
||||
# single line rulesets.
|
||||
SingleLinePerProperty:
|
||||
enabled: true
|
||||
allow_single_line_rule_sets: true
|
||||
|
||||
SingleLinePerSelector:
|
||||
enabled: false
|
||||
|
||||
SpaceAfterComma:
|
||||
enabled: false
|
||||
|
||||
# Properties should be formatted with a single space separating the colon
|
||||
# from the property's value.
|
||||
SpaceAfterPropertyColon:
|
||||
enabled: true
|
||||
|
||||
# Properties should be formatted with no space between the name and the
|
||||
# colon.
|
||||
SpaceAfterPropertyName:
|
||||
enabled: true
|
||||
|
||||
SpaceAroundOperator:
|
||||
enabled: false
|
||||
|
||||
SpaceBeforeBrace:
|
||||
enabled: false
|
||||
|
||||
StringQuotes:
|
||||
enabled: false
|
||||
|
||||
TrailingSemicolon:
|
||||
enabled: false
|
||||
|
||||
TrailingWhitespace:
|
||||
enabled: false
|
||||
|
||||
UnnecessaryMantissa:
|
||||
enabled: false
|
||||
|
||||
UnnecessaryParentReference:
|
||||
enabled: false
|
||||
|
||||
VendorPrefix:
|
||||
enabled: false
|
||||
|
||||
# Omit length units on zero values, e.g. `0px` vs. `0`.
|
||||
ZeroUnit:
|
||||
enabled: true
|
13
CHANGELOG
13
CHANGELOG
|
@ -1,14 +1,15 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.6.0 (unreleased)
|
||||
- Bump gitlab_git to 9.0.3 (Stan Hu)
|
||||
- Support Golang subpackage fetching (Stan Hu)
|
||||
- Bump Capybara gem to 2.6.2 (Stan Hu)
|
||||
- Contributions to forked projects are included in calendar
|
||||
- Improve the formatting for the user page bio (Connor Shea)
|
||||
- Removed the default password from the initial admin account created during
|
||||
setup. A password can be provided during setup (see installation docs), or
|
||||
GitLab will ask the user to create a new one upon first visit.
|
||||
- Fix issue when pushing to projects ending in .wiki
|
||||
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
|
||||
- Don't load all of GitLab in mail_room
|
||||
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
|
||||
- Memoize @group in Admin::GroupsController (Yatish Mehta)
|
||||
|
@ -18,12 +19,19 @@ v 8.6.0 (unreleased)
|
|||
- Return empty array instead of 404 when commit has no statuses in commit status API
|
||||
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
|
||||
- Rewrite logo to simplify SVG code (Sean Lang)
|
||||
- Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
|
||||
- Ignore jobs that start with `.` (hidden jobs)
|
||||
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
|
||||
- Refactor and greatly improve search performance
|
||||
- Add support for cross-project label references
|
||||
- Ensure "new SSH key" email do not ends up as dead Sidekiq jobs
|
||||
- Update documentation to reflect Guest role not being enforced on internal projects
|
||||
- Allow search for logged out users
|
||||
- Allow to define on which builds the current one depends on
|
||||
- Allow user subscription to a label: get notified for issues/merge requests related to that label (Timothy Andrew)
|
||||
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
|
||||
- Don't show Issues/MRs from archived projects in Groups view
|
||||
- Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
|
||||
- Increase the notes polling timeout over time (Roberto Dip)
|
||||
- Add shortcut to toggle markdown preview (Florent Baldino)
|
||||
- Show labels in dashboard and group milestone views
|
||||
|
@ -33,6 +41,9 @@ v 8.6.0 (unreleased)
|
|||
- Create external users which are excluded of internal and private projects unless access was explicitly granted
|
||||
- Continue parameters are checked to ensure redirection goes to the same instance
|
||||
|
||||
v 8.5.6
|
||||
- Obtain a lease before querying LDAP
|
||||
|
||||
v 8.5.5
|
||||
- Ensure removing a project removes associated Todo entries
|
||||
- Prevent a 500 error in Todos when author was removed
|
||||
|
|
|
@ -427,6 +427,7 @@ merge request:
|
|||
1. [Rails](https://github.com/bbatsov/rails-style-guide)
|
||||
1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
|
||||
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
|
||||
1. [SCSS styleguide][scss-styleguide]
|
||||
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
|
||||
contributors to enhance security
|
||||
1. [Database Migrations](doc/development/migration_style_guide.md)
|
||||
|
@ -494,6 +495,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
|
|||
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
|
||||
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
|
||||
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
|
||||
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
|
||||
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
|
||||
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
|
||||
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -77,9 +77,6 @@ gem "haml-rails", '~> 0.9.0'
|
|||
# Files attachments
|
||||
gem "carrierwave", '~> 0.10.0'
|
||||
|
||||
# Image editing
|
||||
gem "mini_magick", '~> 4.4.0'
|
||||
|
||||
# Drag and Drop UI
|
||||
gem 'dropzonejs-rails', '~> 0.7.1'
|
||||
|
||||
|
@ -273,7 +270,7 @@ group :development, :test do
|
|||
# Generate Fake data
|
||||
gem 'ffaker', '~> 2.0.0'
|
||||
|
||||
gem 'capybara', '~> 2.4.0'
|
||||
gem 'capybara', '~> 2.6.2'
|
||||
gem 'capybara-screenshot', '~> 1.0.0'
|
||||
gem 'poltergeist', '~> 1.9.0'
|
||||
|
||||
|
@ -286,6 +283,7 @@ group :development, :test do
|
|||
gem 'spring-commands-teaspoon', '~> 0.0.2'
|
||||
|
||||
gem 'rubocop', '~> 0.35.0', require: false
|
||||
gem 'scss_lint', '~> 0.47.0', require: false
|
||||
gem 'coveralls', '~> 0.8.2', require: false
|
||||
gem 'simplecov', '~> 0.10.0', require: false
|
||||
gem 'flog', require: false
|
||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -108,7 +108,8 @@ GEM
|
|||
thor (~> 0.18)
|
||||
byebug (8.2.1)
|
||||
cal-heatmap-rails (3.5.1)
|
||||
capybara (2.4.4)
|
||||
capybara (2.6.2)
|
||||
addressable
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
|
@ -358,7 +359,7 @@ GEM
|
|||
posix-spawn (~> 0.3)
|
||||
gitlab_emoji (0.3.1)
|
||||
gemojione (~> 2.2, >= 2.2.1)
|
||||
gitlab_git (9.0.1)
|
||||
gitlab_git (9.0.3)
|
||||
activesupport (~> 4.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
github-linguist (~> 4.7.0)
|
||||
|
@ -468,7 +469,6 @@ GEM
|
|||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mimemagic (0.3.0)
|
||||
mini_magick (4.4.0)
|
||||
mini_portile2 (2.0.0)
|
||||
minitest (5.7.0)
|
||||
mousetrap-rails (1.4.6)
|
||||
|
@ -717,6 +717,9 @@ GEM
|
|||
sawyer (0.6.0)
|
||||
addressable (~> 2.3.5)
|
||||
faraday (~> 0.8, < 0.10)
|
||||
scss_lint (0.47.1)
|
||||
rake (>= 0.9, < 11)
|
||||
sass (~> 3.4.15)
|
||||
sdoc (0.3.20)
|
||||
json (>= 1.1.3)
|
||||
rdoc (~> 3.10)
|
||||
|
@ -901,7 +904,7 @@ DEPENDENCIES
|
|||
bundler-audit
|
||||
byebug
|
||||
cal-heatmap-rails (~> 3.5.0)
|
||||
capybara (~> 2.4.0)
|
||||
capybara (~> 2.6.2)
|
||||
capybara-screenshot (~> 1.0.0)
|
||||
carrierwave (~> 0.10.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
|
@ -956,7 +959,6 @@ DEPENDENCIES
|
|||
loofah (~> 2.0.3)
|
||||
mail_room (~> 0.6.1)
|
||||
method_source (~> 0.8)
|
||||
mini_magick (~> 4.4.0)
|
||||
minitest (~> 5.7.0)
|
||||
mousetrap-rails (~> 1.4.6)
|
||||
mysql2 (~> 0.3.16)
|
||||
|
@ -1008,6 +1010,7 @@ DEPENDENCIES
|
|||
ruby-fogbugz (~> 0.2.1)
|
||||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.0)
|
||||
scss_lint (~> 0.47.0)
|
||||
sdoc (~> 0.3.20)
|
||||
seed-fu (~> 2.3.5)
|
||||
select2-rails (~> 3.5.9)
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
#= require jquery.nicescroll
|
||||
#= require_tree .
|
||||
#= require fuzzaldrin-plus
|
||||
#= require cropper.js
|
||||
|
||||
window.slugify = (text) ->
|
||||
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
||||
|
@ -108,6 +107,8 @@ window.onload = ->
|
|||
setTimeout shiftWindow, 100
|
||||
|
||||
$ ->
|
||||
bootstrapBreakpoint = bp.getBreakpointSize()
|
||||
|
||||
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
|
||||
|
||||
# Click a .js-select-on-focus field, select the contents
|
||||
|
@ -256,35 +257,14 @@ $ ->
|
|||
$('.right-sidebar')
|
||||
.hasClass('right-sidebar-collapsed'), { path: '/' })
|
||||
|
||||
bootstrapBreakpoint = undefined;
|
||||
checkBootstrapBreakpoints = ->
|
||||
if $('.device-xs').is(':visible')
|
||||
bootstrapBreakpoint = "xs"
|
||||
else if $('.device-sm').is(':visible')
|
||||
bootstrapBreakpoint = "sm"
|
||||
else if $('.device-md').is(':visible')
|
||||
bootstrapBreakpoint = "md"
|
||||
else if $('.device-lg').is(':visible')
|
||||
bootstrapBreakpoint = "lg"
|
||||
|
||||
setBootstrapBreakpoints = ->
|
||||
if $('.device-xs').length
|
||||
return
|
||||
|
||||
$("body")
|
||||
.append('<div class="device-xs visible-xs"></div>'+
|
||||
'<div class="device-sm visible-sm"></div>'+
|
||||
'<div class="device-md visible-md"></div>'+
|
||||
'<div class="device-lg visible-lg"></div>')
|
||||
checkBootstrapBreakpoints()
|
||||
|
||||
fitSidebarForSize = ->
|
||||
oldBootstrapBreakpoint = bootstrapBreakpoint
|
||||
checkBootstrapBreakpoints()
|
||||
bootstrapBreakpoint = bp.getBreakpointSize()
|
||||
if bootstrapBreakpoint != oldBootstrapBreakpoint
|
||||
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
|
||||
|
||||
checkInitialSidebarSize = ->
|
||||
bootstrapBreakpoint = bp.getBreakpointSize()
|
||||
if bootstrapBreakpoint is "xs" or "sm"
|
||||
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
|
||||
|
||||
|
@ -293,6 +273,5 @@ $ ->
|
|||
.on "resize", (e) ->
|
||||
fitSidebarForSize()
|
||||
|
||||
setBootstrapBreakpoints()
|
||||
checkInitialSidebarSize()
|
||||
new Aside()
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
class @Breakpoints
|
||||
instance = null;
|
||||
|
||||
class BreakpointInstance
|
||||
BREAKPOINTS = ["xs", "sm", "md", "lg"]
|
||||
|
||||
constructor: ->
|
||||
@setup()
|
||||
|
||||
setup: ->
|
||||
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
|
||||
".device-#{breakpoint}"
|
||||
return if $(allDeviceSelector.join(",")).length
|
||||
|
||||
# Create all the elements
|
||||
els = $.map BREAKPOINTS, (breakpoint) ->
|
||||
"<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
|
||||
$("body").append els.join('')
|
||||
|
||||
visibleDevice: ->
|
||||
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
|
||||
".device-#{breakpoint}"
|
||||
$(allDeviceSelector.join(",")).filter(":visible")
|
||||
|
||||
getBreakpointSize: ->
|
||||
$visibleDevice = @visibleDevice
|
||||
# the page refreshed via turbolinks
|
||||
if not $visibleDevice().length
|
||||
@setup()
|
||||
$visibleDevice = @visibleDevice()
|
||||
return $visibleDevice.attr("class").split("visible-")[1]
|
||||
|
||||
@get: ->
|
||||
return instance ?= new BreakpointInstance
|
||||
|
||||
$ =>
|
||||
@bp = Breakpoints.get()
|
|
@ -251,7 +251,7 @@ class GitLabDropdown
|
|||
# Toggle active class for the tick mark
|
||||
el.toggleClass "is-active"
|
||||
|
||||
if value
|
||||
if value?
|
||||
if !field.length
|
||||
# Create hidden input for form
|
||||
input = "<input type='hidden' name='#{fieldName}' />"
|
||||
|
|
|
@ -17,52 +17,14 @@ class @Profile
|
|||
$('.update-notifications').on 'ajax:complete', ->
|
||||
$(this).find('.btn-save').enable()
|
||||
|
||||
# Avatar management
|
||||
$('.js-choose-user-avatar-button').bind "click", ->
|
||||
form = $(this).closest("form")
|
||||
form.find(".js-user-avatar-input").click()
|
||||
|
||||
$avatarInput = $('.js-user-avatar-input')
|
||||
$filename = $('.js-avatar-filename')
|
||||
$modalCrop = $('.modal-profile-crop')
|
||||
$modalCropImg = $('.modal-profile-crop-image')
|
||||
|
||||
$('.js-choose-user-avatar-button').on "click", ->
|
||||
$form = $(this).closest("form")
|
||||
$form.find(".js-user-avatar-input").click()
|
||||
|
||||
$modalCrop.on 'shown.bs.modal', ->
|
||||
setTimeout ( -> # The cropper must be asynchronously initialized
|
||||
$modalCropImg.cropper
|
||||
aspectRatio: 1
|
||||
modal: false
|
||||
scalable: false
|
||||
rotatable: false
|
||||
zoomable: false
|
||||
|
||||
crop: (event) ->
|
||||
['x', 'y'].forEach (key) ->
|
||||
$("#user_avatar_crop_#{key}").val(Math.floor(event[key]))
|
||||
$("#user_avatar_crop_size").val(Math.floor(event.width))
|
||||
), 0
|
||||
|
||||
$modalCrop.on 'hidden.bs.modal', ->
|
||||
$modalCropImg.attr('src', '').cropper('destroy')
|
||||
$avatarInput.val('')
|
||||
$filename.text($filename.data('label'))
|
||||
|
||||
$('.js-upload-user-avatar').on 'click', ->
|
||||
$('.edit-user').submit()
|
||||
|
||||
$avatarInput.on "change", ->
|
||||
$('.js-user-avatar-input').bind "change", ->
|
||||
form = $(this).closest("form")
|
||||
filename = $(this).val().replace(/^.*[\\\/]/, '')
|
||||
$filename.data('label', $filename.text()).text(filename)
|
||||
|
||||
reader = new FileReader
|
||||
|
||||
reader.onload = (event) ->
|
||||
$modalCrop.modal('show')
|
||||
$modalCropImg.attr('src', event.target.result)
|
||||
|
||||
fileData = reader.readAsDataURL(this.files[0])
|
||||
form.find(".js-avatar-filename").text(filename)
|
||||
|
||||
$ ->
|
||||
# Extract the SSH Key title from its comment
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
$(document).on("click", '.toggle-nav-collapse', (e) ->
|
||||
e.preventDefault()
|
||||
collapsed = 'page-sidebar-collapsed'
|
||||
expanded = 'page-sidebar-expanded'
|
||||
collapsed = 'page-sidebar-collapsed'
|
||||
expanded = 'page-sidebar-expanded'
|
||||
|
||||
toggleSidebar = ->
|
||||
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
|
||||
$('header').toggleClass("header-collapsed header-expanded")
|
||||
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
|
||||
|
@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
|
|||
niceScrollBars.updateScrollBar();
|
||||
), 300
|
||||
|
||||
$(document).on("click", '.toggle-nav-collapse', (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
toggleSidebar()
|
||||
)
|
||||
|
||||
$ ->
|
||||
size = bp.getBreakpointSize()
|
||||
|
||||
if size is "xs" or size is "sm"
|
||||
if $('.page-with-sidebar').hasClass(expanded)
|
||||
toggleSidebar()
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
class @Subscription
|
||||
constructor: (url) ->
|
||||
$(".subscribe-button").unbind("click").click (event)=>
|
||||
btn = $(event.currentTarget)
|
||||
action = btn.find("span").text()
|
||||
current_status = $(".subscription-status").attr("data-status")
|
||||
btn.prop("disabled", true)
|
||||
|
||||
$.post url, =>
|
||||
btn.prop("disabled", false)
|
||||
status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
|
||||
$(".subscription-status").attr("data-status", status)
|
||||
action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
|
||||
btn.find("span").text(action)
|
||||
$(".subscription-status>div").toggleClass("hidden")
|
||||
constructor: (container) ->
|
||||
$container = $(container)
|
||||
@url = $container.attr('data-url')
|
||||
@subscribe_button = $container.find('.subscribe-button')
|
||||
@subscription_status = $container.find('.subscription-status')
|
||||
@subscribe_button.unbind('click').click(@toggleSubscription)
|
||||
|
||||
|
||||
toggleSubscription: (event) =>
|
||||
btn = $(event.currentTarget)
|
||||
action = btn.find('span').text()
|
||||
current_status = @subscription_status.attr('data-status')
|
||||
btn.prop('disabled', true)
|
||||
|
||||
$.post @url, =>
|
||||
btn.prop('disabled', false)
|
||||
status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
|
||||
@subscription_status.attr('data-status', status)
|
||||
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
|
||||
btn.find('span').text(action)
|
||||
@subscription_status.find('>div').toggleClass('hidden')
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
*= require_self
|
||||
*= require dropzone/basic
|
||||
*= require cal-heatmap
|
||||
*= require cropper.css
|
||||
*/
|
||||
|
||||
/*
|
||||
|
|
|
@ -120,6 +120,10 @@
|
|||
.cover-desc {
|
||||
padding: 0 $gl-padding 3px;
|
||||
color: $gl-text-color;
|
||||
|
||||
&.username:last-child {
|
||||
padding-bottom: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-controls {
|
||||
|
|
|
@ -141,22 +141,18 @@ header {
|
|||
margin-left: $sidebar_collapsed_width;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-md-max) {
|
||||
.header-collapsed {
|
||||
margin-left: $sidebar_collapsed_width;
|
||||
}
|
||||
.header-collapsed {
|
||||
margin-left: $sidebar_collapsed_width;
|
||||
|
||||
.header-expanded {
|
||||
margin-left: $sidebar_width;
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: $screen-md-max) {
|
||||
.header-collapsed {
|
||||
@media (min-width: $screen-md-min) {
|
||||
@include collapsed-header;
|
||||
}
|
||||
}
|
||||
|
||||
.header-expanded {
|
||||
.header-expanded {
|
||||
margin-left: $sidebar_collapsed_width;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
margin-left: $sidebar_width;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,12 +41,6 @@
|
|||
transition: $transition;
|
||||
}
|
||||
|
||||
@mixin transform($transform) {
|
||||
-webkit-transform: $transform;
|
||||
-ms-transform: $transform;
|
||||
transform: $transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefilled mixins
|
||||
* Mixins with fixed values
|
||||
|
|
|
@ -34,12 +34,12 @@
|
|||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: $gutter_width;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
z-index: 99;
|
||||
z-index: 999;
|
||||
background: $background-color;
|
||||
}
|
||||
|
||||
|
@ -203,7 +203,11 @@
|
|||
}
|
||||
|
||||
@mixin expanded-sidebar {
|
||||
padding-left: $sidebar_width;
|
||||
padding-left: $sidebar_collapsed_width;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
padding-left: $sidebar_width;
|
||||
}
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
/* Extra small devices (phones, less than 768px) */
|
||||
|
|
|
@ -103,6 +103,10 @@ $border-red-dark: #CA264F;
|
|||
$help-well-bg: #FAFAFA;
|
||||
$help-well-border: #E5E5E5;
|
||||
|
||||
$warning-message-bg: #FBF2D9;
|
||||
$warning-message-color: #9E8E60;
|
||||
$warning-message-border: #F0E2BB;
|
||||
|
||||
/* header */
|
||||
$light-grey-header: #faf9f9;
|
||||
|
||||
|
|
|
@ -41,3 +41,7 @@
|
|||
.color-label {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.label-subscription {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
|
@ -109,42 +109,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.modal-profile-crop {
|
||||
.modal-dialog {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
p {
|
||||
display: table;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.cropper-bg {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.cropper-crop-box {
|
||||
box-sizing: content-box;
|
||||
border: 999px solid transparentize(#ccc, 0.5);
|
||||
@include transform(translate(-999px, -999px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.modal-profile-crop .modal-dialog {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.key-list-item {
|
||||
.key-list-item-info {
|
||||
@media (min-width: $screen-sm-min) {
|
||||
|
@ -215,3 +179,21 @@
|
|||
color: $provider-btn-not-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-settings-message {
|
||||
line-height: 32px;
|
||||
color: $warning-message-color;
|
||||
background-color: $warning-message-bg;
|
||||
border: 1px solid $warning-message-border;
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.oauth-applications {
|
||||
form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.last-heading {
|
||||
width: 105px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
module ToggleSubscriptionAction
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def toggle_subscription
|
||||
return unless current_user
|
||||
|
||||
subscribable_resource.toggle_subscription(current_user)
|
||||
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subscribable_resource
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
|
@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
|||
layout 'profile'
|
||||
|
||||
def index
|
||||
head :forbidden and return
|
||||
set_index_vars
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
|||
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
|
||||
redirect_to oauth_application_url(@application)
|
||||
else
|
||||
render :new
|
||||
set_index_vars
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @application.destroy
|
||||
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
|
||||
end
|
||||
|
||||
redirect_to applications_profile_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_user_oauth_applications_enabled
|
||||
|
@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
|||
redirect_to applications_profile_url
|
||||
end
|
||||
|
||||
def set_index_vars
|
||||
@applications = current_user.oauth_applications
|
||||
@authorized_tokens = current_user.oauth_authorized_tokens
|
||||
@authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
|
||||
@authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
|
||||
|
||||
# Don't overwrite a value possibly set by `create`
|
||||
@application ||= Doorkeeper::Application.new
|
||||
end
|
||||
|
||||
# Override Doorkeeper to scope to the current user
|
||||
def set_application
|
||||
@application = current_user.oauth_applications.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController
|
|||
def show
|
||||
end
|
||||
|
||||
def applications
|
||||
@applications = current_user.oauth_applications
|
||||
@authorized_tokens = current_user.oauth_authorized_tokens
|
||||
@authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
|
||||
@authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
|
||||
end
|
||||
|
||||
def update
|
||||
user_params.except!(:email) if @user.ldap_user?
|
||||
|
||||
|
@ -65,9 +58,6 @@ class ProfilesController < Profiles::ApplicationController
|
|||
|
||||
def user_params
|
||||
params.require(:user).permit(
|
||||
:avatar_crop_x,
|
||||
:avatar_crop_y,
|
||||
:avatar_crop_size,
|
||||
:avatar,
|
||||
:bio,
|
||||
:email,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class Projects::IssuesController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :issue, only: [:edit, :update, :show, :toggle_subscription]
|
||||
before_action :issue, only: [:edit, :update, :show]
|
||||
|
||||
# Allow read any issue
|
||||
before_action :authorize_read_issue!
|
||||
|
@ -110,12 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
|
||||
end
|
||||
|
||||
def toggle_subscription
|
||||
@issue.toggle_subscription(current_user)
|
||||
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def closed_by_merge_requests
|
||||
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
|
||||
end
|
||||
|
@ -129,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
redirect_old
|
||||
end
|
||||
end
|
||||
alias_method :subscribable_resource, :issue
|
||||
|
||||
def authorize_update_issue!
|
||||
return render_404 unless can?(current_user, :update_issue, @issue)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
class Projects::LabelsController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :label, only: [:edit, :update, :destroy]
|
||||
before_action :authorize_read_label!
|
||||
before_action :authorize_admin_labels!, except: [:index]
|
||||
before_action :authorize_admin_labels!, only: [
|
||||
:new, :create, :edit, :update, :generate, :destroy
|
||||
]
|
||||
|
||||
respond_to :js, :html
|
||||
|
||||
|
@ -73,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def label
|
||||
@label = @project.labels.find(params[:id])
|
||||
@label ||= @project.labels.find(params[:id])
|
||||
end
|
||||
alias_method :subscribable_resource, :label
|
||||
|
||||
def authorize_admin_labels!
|
||||
return render_404 unless can?(current_user, :admin_label, @project)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
include DiffHelper
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :merge_request, only: [
|
||||
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
|
||||
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
|
||||
:ci_status, :cancel_merge_when_build_succeeds
|
||||
]
|
||||
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
|
||||
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
|
||||
|
@ -233,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
render json: response
|
||||
end
|
||||
|
||||
def toggle_subscription
|
||||
@merge_request.toggle_subscription(current_user)
|
||||
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def selected_target_project
|
||||
|
@ -252,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
def merge_request
|
||||
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
|
||||
end
|
||||
alias_method :subscribable_resource, :merge_request
|
||||
|
||||
def closes_issues
|
||||
@closes_issues ||= @merge_request.closes_issues
|
||||
|
|
|
@ -172,10 +172,15 @@ class ProjectsController < ApplicationController
|
|||
def housekeeping
|
||||
::Projects::HousekeepingService.new(@project).execute
|
||||
|
||||
respond_to do |format|
|
||||
flash[:notice] = "Housekeeping successfully started."
|
||||
format.html { redirect_to project_path(@project) }
|
||||
end
|
||||
redirect_to(
|
||||
project_path(@project),
|
||||
notice: "Housekeeping successfully started"
|
||||
)
|
||||
rescue ::Projects::HousekeepingService::LeaseTaken => ex
|
||||
redirect_to(
|
||||
edit_project_path(@project),
|
||||
alert: ex.to_s
|
||||
)
|
||||
end
|
||||
|
||||
def toggle_star
|
||||
|
|
|
@ -12,9 +12,13 @@ module CiStatusHelper
|
|||
ci_label_for_status(ci_commit.status)
|
||||
end
|
||||
|
||||
def ci_status_with_icon(status)
|
||||
content_tag :span, class: "ci-status ci-#{status}" do
|
||||
ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status)
|
||||
def ci_status_with_icon(status, target = nil)
|
||||
content = ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status)
|
||||
klass = "ci-status ci-#{status}"
|
||||
if target
|
||||
link_to content, target, class: klass
|
||||
else
|
||||
content_tag :span, content, class: klass
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ module EventsHelper
|
|||
author = event.author
|
||||
|
||||
if author
|
||||
link_to author.name, user_path(author.username)
|
||||
link_to author.name, user_path(author.username), title: h(author.name)
|
||||
else
|
||||
event.author_name
|
||||
end
|
||||
|
@ -159,7 +159,7 @@ module EventsHelper
|
|||
link_to(
|
||||
namespace_project_commit_path(event.project.namespace, event.project,
|
||||
event.note_commit_id,
|
||||
anchor: dom_id(event.target)),
|
||||
anchor: dom_id(event.target), title: h(event.target_title)),
|
||||
class: "commit_short_id"
|
||||
) do
|
||||
"#{event.note_target_type} #{event.note_short_commit_id}"
|
||||
|
@ -167,7 +167,7 @@ module EventsHelper
|
|||
elsif event.note_project_snippet?
|
||||
link_to(namespace_project_snippet_path(event.project.namespace,
|
||||
event.project,
|
||||
event.note_target)) do
|
||||
event.note_target), title: h(event.project.name)) do
|
||||
"#{event.note_target_type} #{truncate event.note_target.to_reference}"
|
||||
end
|
||||
else
|
||||
|
|
|
@ -31,7 +31,11 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
def issuable_state_scope(issuable)
|
||||
issuable.open? ? :opened : :closed
|
||||
if issuable.respond_to?(:merged?) && issuable.merged?
|
||||
:merged
|
||||
else
|
||||
issuable.open? ? :opened : :closed
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -124,6 +124,14 @@ module LabelsHelper
|
|||
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
|
||||
end
|
||||
|
||||
def label_subscription_status(label)
|
||||
label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
|
||||
end
|
||||
|
||||
def label_subscription_toggle_button_text(label)
|
||||
label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
|
||||
end
|
||||
|
||||
# Required for Banzai::Filter::LabelReferenceFilter
|
||||
module_function :render_colored_label, :render_colored_cross_project_label,
|
||||
:text_color_for_bg, :escape_once
|
||||
|
|
|
@ -8,7 +8,7 @@ module ProjectsHelper
|
|||
end
|
||||
|
||||
def link_to_project(project)
|
||||
link_to [project.namespace.becomes(Namespace), project] do
|
||||
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
|
||||
title = content_tag(:span, project.name, class: 'project-name')
|
||||
|
||||
if project.namespace
|
||||
|
|
|
@ -16,7 +16,7 @@ module TodosHelper
|
|||
|
||||
def todo_target_link(todo)
|
||||
target = todo.target_type.titleize.downcase
|
||||
link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo)
|
||||
link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) }
|
||||
end
|
||||
|
||||
def todo_target_path(todo)
|
||||
|
|
|
@ -16,7 +16,15 @@ module Emails
|
|||
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
|
||||
setup_issue_mail(issue_id, recipient_id)
|
||||
|
||||
@updated_by = User.find updated_by_user_id
|
||||
@updated_by = User.find(updated_by_user_id)
|
||||
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
|
||||
setup_issue_mail(issue_id, recipient_id)
|
||||
|
||||
@label_names = label_names
|
||||
@labels_url = namespace_project_labels_url(@project.namespace, @project)
|
||||
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
|
@ -24,20 +32,12 @@ module Emails
|
|||
setup_issue_mail(issue_id, recipient_id)
|
||||
|
||||
@issue_status = status
|
||||
@updated_by = User.find updated_by_user_id
|
||||
@updated_by = User.find(updated_by_user_id)
|
||||
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issue_thread_options(sender_id, recipient_id)
|
||||
{
|
||||
from: sender(sender_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@issue.title} (##{@issue.iid})")
|
||||
}
|
||||
end
|
||||
|
||||
def setup_issue_mail(issue_id, recipient_id)
|
||||
@issue = Issue.find(issue_id)
|
||||
@project = @issue.project
|
||||
|
@ -45,5 +45,13 @@ module Emails
|
|||
|
||||
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
|
||||
end
|
||||
|
||||
def issue_thread_options(sender_id, recipient_id)
|
||||
{
|
||||
from: sender(sender_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@issue.title} (##{@issue.iid})")
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,50 +3,43 @@ module Emails
|
|||
def new_merge_request_email(recipient_id, merge_request_id)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
mail_new_thread(@merge_request,
|
||||
from: sender(@merge_request.author_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
|
||||
mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
|
||||
end
|
||||
|
||||
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
|
||||
mail_answer_thread(@merge_request,
|
||||
from: sender(updated_by_user_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@label_names = label_names
|
||||
@labels_url = namespace_project_labels_url(@project.namespace, @project)
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@updated_by = User.find updated_by_user_id
|
||||
mail_answer_thread(@merge_request,
|
||||
from: sender(updated_by_user_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
|
||||
@updated_by = User.find(updated_by_user_id)
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
mail_answer_thread(@merge_request,
|
||||
from: sender(updated_by_user_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@mr_status = status
|
||||
@updated_by = User.find updated_by_user_id
|
||||
mail_answer_thread(@merge_request,
|
||||
from: sender(updated_by_user_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
|
||||
@updated_by = User.find(updated_by_user_id)
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -54,11 +47,17 @@ module Emails
|
|||
def setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
@merge_request = MergeRequest.find(merge_request_id)
|
||||
@project = @merge_request.project
|
||||
@target_url = namespace_project_merge_request_url(@project.namespace,
|
||||
@project,
|
||||
@merge_request)
|
||||
@target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
|
||||
|
||||
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
|
||||
end
|
||||
|
||||
def merge_request_thread_options(sender_id, recipient_id)
|
||||
{
|
||||
from: sender(sender_id),
|
||||
to: recipient(recipient_id),
|
||||
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,10 @@ module Emails
|
|||
end
|
||||
|
||||
def new_ssh_key_email(key_id)
|
||||
@key = Key.find(key_id)
|
||||
@key = Key.find_by_id(key_id)
|
||||
|
||||
return unless @key
|
||||
|
||||
@current_user = @user = @key.user
|
||||
@target_url = user_url(@user)
|
||||
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
|
||||
module Ci
|
||||
class Build < CommitStatus
|
||||
include Gitlab::Application.routes.url_helpers
|
||||
|
||||
LAZY_ATTRIBUTES = ['trace']
|
||||
|
||||
belongs_to :runner, class_name: 'Ci::Runner'
|
||||
|
@ -128,7 +126,7 @@ module Ci
|
|||
end
|
||||
|
||||
def retried?
|
||||
!self.commit.latest_builds_for_ref(self.ref).include?(self)
|
||||
!self.commit.latest_statuses_for_ref(self.ref).include?(self)
|
||||
end
|
||||
|
||||
def depends_on_builds
|
||||
|
@ -309,22 +307,6 @@ module Ci
|
|||
project.valid_runners_token? token
|
||||
end
|
||||
|
||||
def target_url
|
||||
namespace_project_build_url(project.namespace, project, self)
|
||||
end
|
||||
|
||||
def cancel_url
|
||||
if active?
|
||||
cancel_namespace_project_build_path(project.namespace, project, self)
|
||||
end
|
||||
end
|
||||
|
||||
def retry_url
|
||||
if retryable?
|
||||
retry_namespace_project_build_path(project.namespace, project, self)
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_served?(runner)
|
||||
(tag_list - runner.tag_list).empty?
|
||||
end
|
||||
|
@ -333,7 +315,7 @@ module Ci
|
|||
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
|
||||
end
|
||||
|
||||
def show_warning?
|
||||
def stuck?
|
||||
pending? && !any_runners_online?
|
||||
end
|
||||
|
||||
|
@ -348,18 +330,6 @@ module Ci
|
|||
artifacts_file.exists?
|
||||
end
|
||||
|
||||
def artifacts_download_url
|
||||
if artifacts?
|
||||
download_namespace_project_build_artifacts_path(project.namespace, project, self)
|
||||
end
|
||||
end
|
||||
|
||||
def artifacts_browse_url
|
||||
if artifacts_metadata?
|
||||
browse_namespace_project_build_artifacts_path(project.namespace, project, self)
|
||||
end
|
||||
end
|
||||
|
||||
def artifacts_metadata?
|
||||
artifacts? && artifacts_metadata.exists?
|
||||
end
|
||||
|
|
|
@ -25,8 +25,6 @@ module Ci
|
|||
has_many :builds, class_name: 'Ci::Build'
|
||||
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
|
||||
|
||||
scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
|
||||
|
||||
validates_presence_of :sha
|
||||
validate :valid_commit_sha
|
||||
|
||||
|
@ -42,16 +40,6 @@ module Ci
|
|||
project.id
|
||||
end
|
||||
|
||||
def last_build
|
||||
builds.order(:id).last
|
||||
end
|
||||
|
||||
def retry
|
||||
latest_builds.each do |build|
|
||||
Ci::Build.retry(build)
|
||||
end
|
||||
end
|
||||
|
||||
def valid_commit_sha
|
||||
if self.sha == Gitlab::Git::BLANK_SHA
|
||||
self.errors.add(:sha, " cant be 00000000 (branch removal)")
|
||||
|
@ -121,12 +109,14 @@ module Ci
|
|||
@latest_statuses ||= statuses.latest.to_a
|
||||
end
|
||||
|
||||
def latest_builds
|
||||
@latest_builds ||= builds.latest.to_a
|
||||
def latest_statuses_for_ref(ref)
|
||||
latest_statuses.select { |status| status.ref == ref }
|
||||
end
|
||||
|
||||
def latest_builds_for_ref(ref)
|
||||
latest_builds.select { |build| build.ref == ref }
|
||||
def matrix_builds(build = nil)
|
||||
matrix_builds = builds.latest.ordered
|
||||
matrix_builds = matrix_builds.similar(build) if build
|
||||
matrix_builds.to_a
|
||||
end
|
||||
|
||||
def retried
|
||||
|
@ -170,7 +160,7 @@ module Ci
|
|||
end
|
||||
|
||||
def duration
|
||||
duration_array = latest_statuses.map(&:duration).compact
|
||||
duration_array = statuses.map(&:duration).compact
|
||||
duration_array.reduce(:+).to_i
|
||||
end
|
||||
|
||||
|
@ -183,16 +173,12 @@ module Ci
|
|||
end
|
||||
|
||||
def coverage
|
||||
coverage_array = latest_builds.map(&:coverage).compact
|
||||
coverage_array = latest_statuses.map(&:coverage).compact
|
||||
if coverage_array.size >= 1
|
||||
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
|
||||
end
|
||||
end
|
||||
|
||||
def matrix_for_ref?(ref)
|
||||
latest_builds_for_ref(ref).size > 1
|
||||
end
|
||||
|
||||
def config_processor
|
||||
return nil unless ci_yaml_file
|
||||
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
|
||||
|
@ -218,10 +204,6 @@ module Ci
|
|||
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
|
||||
end
|
||||
|
||||
def update_committed!
|
||||
update!(committed_at: DateTime.now)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_yaml_error(error)
|
||||
|
|
|
@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def cancel_url
|
||||
nil
|
||||
end
|
||||
|
||||
def retry_url
|
||||
nil
|
||||
end
|
||||
|
||||
def show_warning?
|
||||
def stuck?
|
||||
false
|
||||
end
|
||||
|
||||
def artifacts_download_url
|
||||
nil
|
||||
end
|
||||
|
||||
def artifacts_browse_url
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ module Issuable
|
|||
extend ActiveSupport::Concern
|
||||
include Participable
|
||||
include Mentionable
|
||||
include Subscribable
|
||||
include StripAttribute
|
||||
|
||||
included do
|
||||
|
@ -18,7 +19,6 @@ module Issuable
|
|||
has_many :notes, as: :noteable, dependent: :destroy
|
||||
has_many :label_links, as: :target, dependent: :destroy
|
||||
has_many :labels, through: :label_links
|
||||
has_many :subscriptions, dependent: :destroy, as: :subscribable
|
||||
|
||||
validates :author, presence: true
|
||||
validates :title, presence: true, length: { within: 0..255 }
|
||||
|
@ -149,28 +149,10 @@ module Issuable
|
|||
notes.awards.where(note: "thumbsup").count
|
||||
end
|
||||
|
||||
def subscribed?(user)
|
||||
subscription = subscriptions.find_by_user_id(user.id)
|
||||
|
||||
if subscription
|
||||
return subscription.subscribed
|
||||
end
|
||||
|
||||
def subscribed_without_subscriptions?(user)
|
||||
participants(user).include?(user)
|
||||
end
|
||||
|
||||
def toggle_subscription(user)
|
||||
subscriptions.
|
||||
find_or_initialize_by(user_id: user.id).
|
||||
update(subscribed: !subscribed?(user))
|
||||
end
|
||||
|
||||
def unsubscribe(user)
|
||||
subscriptions.
|
||||
find_or_initialize_by(user_id: user.id).
|
||||
update(subscribed: false)
|
||||
end
|
||||
|
||||
def to_hook_data(user)
|
||||
hook_data = {
|
||||
object_kind: self.class.name.underscore,
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# == Subscribable concern
|
||||
#
|
||||
# Users can subscribe to these models.
|
||||
#
|
||||
# Used by Issue, MergeRequest, Label
|
||||
#
|
||||
|
||||
module Subscribable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :subscriptions, dependent: :destroy, as: :subscribable
|
||||
end
|
||||
|
||||
def subscribed?(user)
|
||||
if subscription = subscriptions.find_by_user_id(user.id)
|
||||
subscription.subscribed
|
||||
else
|
||||
subscribed_without_subscriptions?(user)
|
||||
end
|
||||
end
|
||||
|
||||
# Override this method to define custom logic to consider a subscribable as
|
||||
# subscribed without an explicit subscription record.
|
||||
def subscribed_without_subscriptions?(user)
|
||||
false
|
||||
end
|
||||
|
||||
def subscribers
|
||||
subscriptions.where(subscribed: true).map(&:user)
|
||||
end
|
||||
|
||||
def toggle_subscription(user)
|
||||
subscriptions.
|
||||
find_or_initialize_by(user_id: user.id).
|
||||
update(subscribed: !subscribed?(user))
|
||||
end
|
||||
|
||||
def unsubscribe(user)
|
||||
subscriptions.
|
||||
find_or_initialize_by(user_id: user.id).
|
||||
update(subscribed: false)
|
||||
end
|
||||
end
|
|
@ -16,6 +16,7 @@
|
|||
require 'digest/md5'
|
||||
|
||||
class Key < ActiveRecord::Base
|
||||
include AfterCommitQueue
|
||||
include Sortable
|
||||
|
||||
belongs_to :user
|
||||
|
@ -62,7 +63,7 @@ class Key < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def notify_user
|
||||
NotificationService.new.new_key(self)
|
||||
run_after_commit { NotificationService.new.new_key(self) }
|
||||
end
|
||||
|
||||
def post_create_hook
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
class Label < ActiveRecord::Base
|
||||
include Referable
|
||||
include Subscribable
|
||||
|
||||
# Represents a "No Label" state used for filtering Issues and Merge
|
||||
# Requests that have no label assigned.
|
||||
LabelStruct = Struct.new(:title, :name)
|
||||
|
|
|
@ -280,7 +280,14 @@ class Project < ActiveRecord::Base
|
|||
or(ptable[:description].matches(pattern))
|
||||
)
|
||||
|
||||
# We explicitly remove any eager loading clauses as they're:
|
||||
#
|
||||
# 1. Not needed by this query
|
||||
# 2. Combined with .joins(:namespace) lead to all columns from the
|
||||
# projects & namespaces tables being selected, leading to a SQL error
|
||||
# due to the columns of all UNION'd queries no longer being the same.
|
||||
namespaces = select(:id).
|
||||
except(:includes).
|
||||
joins(:namespace).
|
||||
where(ntable[:name].matches(pattern))
|
||||
|
||||
|
@ -502,6 +509,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def external_issue_tracker
|
||||
return @external_issue_tracker if defined?(@external_issue_tracker)
|
||||
@external_issue_tracker ||=
|
||||
services.issue_trackers.active.without_defaults.first
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base
|
|||
belongs_to :user
|
||||
belongs_to :subscribable, polymorphic: true
|
||||
|
||||
validates :user_id,
|
||||
validates :user_id,
|
||||
uniqueness: { scope: [:subscribable_id, :subscribable_type] },
|
||||
presence: true
|
||||
end
|
||||
|
|
|
@ -100,9 +100,6 @@ class User < ActiveRecord::Base
|
|||
# Virtual attribute for authenticating by either username or email
|
||||
attr_accessor :login
|
||||
|
||||
# Virtual attributes to define avatar cropping
|
||||
attr_accessor :avatar_crop_x, :avatar_crop_y, :avatar_crop_size
|
||||
|
||||
#
|
||||
# Relations
|
||||
#
|
||||
|
@ -168,11 +165,6 @@ class User < ActiveRecord::Base
|
|||
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
|
||||
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
|
||||
|
||||
validates :avatar_crop_x, :avatar_crop_y, :avatar_crop_size,
|
||||
numericality: { only_integer: true },
|
||||
presence: true,
|
||||
if: ->(user) { user.avatar? && user.avatar_changed? }
|
||||
|
||||
before_validation :generate_password, on: :create
|
||||
before_validation :restricted_signup_domains, on: :create
|
||||
before_validation :sanitize_attrs
|
||||
|
|
|
@ -3,7 +3,7 @@ module Ci
|
|||
def execute(project, opts)
|
||||
sha = opts[:sha] || ref_sha(project, opts[:ref])
|
||||
|
||||
commit = project.ci_commits.ordered.find_by(sha: sha)
|
||||
commit = project.ci_commits.find_by(sha: sha)
|
||||
image_name = image_for_commit(commit)
|
||||
|
||||
image_path = Rails.root.join('public/ci', image_name)
|
||||
|
|
|
@ -33,7 +33,6 @@ class CreateCommitBuildsService
|
|||
unless commit.skip_ci?
|
||||
# Create builds for commit
|
||||
tag = Gitlab::Git.tag_ref?(origin_ref)
|
||||
commit.update_committed!
|
||||
commit.create_builds(ref, tag, user)
|
||||
end
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ class GitPushService < BaseService
|
|||
# Update merge requests that may be affected by this push. A new branch
|
||||
# could cause the last commit of a merge request to change.
|
||||
update_merge_requests
|
||||
|
||||
perform_housekeeping
|
||||
end
|
||||
|
||||
def update_main_language
|
||||
|
@ -73,6 +75,13 @@ class GitPushService < BaseService
|
|||
ProjectCacheWorker.perform_async(@project.id)
|
||||
end
|
||||
|
||||
def perform_housekeeping
|
||||
housekeeping = Projects::HousekeepingService.new(@project)
|
||||
housekeeping.increment!
|
||||
housekeeping.execute if housekeeping.needed?
|
||||
rescue Projects::HousekeepingService::LeaseTaken
|
||||
end
|
||||
|
||||
def process_default_branch
|
||||
@push_commits = project.repository.commits(params[:newrev])
|
||||
|
||||
|
@ -80,7 +89,7 @@ class GitPushService < BaseService
|
|||
project.change_head(branch_name)
|
||||
|
||||
# Set protection on the default branch if configured
|
||||
if (current_application_settings.default_branch_protection != PROTECTION_NONE)
|
||||
if current_application_settings.default_branch_protection != PROTECTION_NONE
|
||||
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
|
||||
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
|
||||
end
|
||||
|
|
|
@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
|
|||
issuable, issuable.project, current_user, issuable.milestone)
|
||||
end
|
||||
|
||||
def create_labels_note(issuable, added_labels, removed_labels)
|
||||
def create_labels_note(issuable, old_labels)
|
||||
added_labels = issuable.labels - old_labels
|
||||
removed_labels = old_labels - issuable.labels
|
||||
|
||||
SystemNoteService.change_label(
|
||||
issuable, issuable.project, current_user, added_labels, removed_labels)
|
||||
end
|
||||
|
@ -71,20 +74,19 @@ class IssuableBaseService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def has_changes?(issuable, options = {})
|
||||
def has_changes?(issuable, old_labels: [])
|
||||
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
|
||||
|
||||
attrs_changed = valid_attrs.any? do |attr|
|
||||
issuable.previous_changes.include?(attr.to_s)
|
||||
end
|
||||
|
||||
old_labels = options[:old_labels]
|
||||
labels_changed = old_labels && issuable.labels != old_labels
|
||||
labels_changed = issuable.labels != old_labels
|
||||
|
||||
attrs_changed || labels_changed
|
||||
end
|
||||
|
||||
def handle_common_system_notes(issuable, options = {})
|
||||
def handle_common_system_notes(issuable, old_labels: [])
|
||||
if issuable.previous_changes.include?('title')
|
||||
create_title_change_note(issuable, issuable.previous_changes['title'].first)
|
||||
end
|
||||
|
@ -93,9 +95,6 @@ class IssuableBaseService < BaseService
|
|||
create_task_status_note(issuable)
|
||||
end
|
||||
|
||||
old_labels = options[:old_labels]
|
||||
if old_labels && (issuable.labels != old_labels)
|
||||
create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
|
||||
end
|
||||
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,8 +4,8 @@ module Issues
|
|||
update(issue)
|
||||
end
|
||||
|
||||
def handle_changes(issue, options = {})
|
||||
if has_changes?(issue, options)
|
||||
def handle_changes(issue, old_labels: [])
|
||||
if has_changes?(issue, old_labels: old_labels)
|
||||
todo_service.mark_pending_todos_as_done(issue, current_user)
|
||||
end
|
||||
|
||||
|
@ -23,6 +23,11 @@ module Issues
|
|||
notification_service.reassigned_issue(issue, current_user)
|
||||
todo_service.reassigned_issue(issue, current_user)
|
||||
end
|
||||
|
||||
added_labels = issue.labels - old_labels
|
||||
if added_labels.present?
|
||||
notification_service.relabeled_issue(issue, added_labels, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def reopen_service
|
||||
|
|
|
@ -14,8 +14,8 @@ module MergeRequests
|
|||
update(merge_request)
|
||||
end
|
||||
|
||||
def handle_changes(merge_request, options = {})
|
||||
if has_changes?(merge_request, options)
|
||||
def handle_changes(merge_request, old_labels: [])
|
||||
if has_changes?(merge_request, old_labels: old_labels)
|
||||
todo_service.mark_pending_todos_as_done(merge_request, current_user)
|
||||
end
|
||||
|
||||
|
@ -44,6 +44,15 @@ module MergeRequests
|
|||
merge_request.previous_changes.include?('source_branch')
|
||||
merge_request.mark_as_unchecked
|
||||
end
|
||||
|
||||
added_labels = merge_request.labels - old_labels
|
||||
if added_labels.present?
|
||||
notification_service.relabeled_merge_request(
|
||||
merge_request,
|
||||
added_labels,
|
||||
current_user
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def reopen_service
|
||||
|
|
|
@ -24,16 +24,17 @@ class NotificationService
|
|||
end
|
||||
end
|
||||
|
||||
# When create an issue we should send next emails:
|
||||
# When create an issue we should send an email to:
|
||||
#
|
||||
# * issue assignee if their notification level is not Disabled
|
||||
# * project team members with notification level higher then Participating
|
||||
# * watchers of the issue's labels
|
||||
#
|
||||
def new_issue(issue, current_user)
|
||||
new_resource_email(issue, issue.project, 'new_issue_email')
|
||||
end
|
||||
|
||||
# When we close an issue we should send next emails:
|
||||
# When we close an issue we should send an email to:
|
||||
#
|
||||
# * issue author if their notification level is not Disabled
|
||||
# * issue assignee if their notification level is not Disabled
|
||||
|
@ -43,7 +44,7 @@ class NotificationService
|
|||
close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
|
||||
end
|
||||
|
||||
# When we reassign an issue we should send next emails:
|
||||
# When we reassign an issue we should send an email to:
|
||||
#
|
||||
# * issue old assignee if their notification level is not Disabled
|
||||
# * issue new assignee if their notification level is not Disabled
|
||||
|
@ -52,16 +53,25 @@ class NotificationService
|
|||
reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
|
||||
end
|
||||
|
||||
# When we add labels to an issue we should send an email to:
|
||||
#
|
||||
# * watchers of the issue's labels
|
||||
#
|
||||
def relabeled_issue(issue, added_labels, current_user)
|
||||
relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
|
||||
end
|
||||
|
||||
# When create a merge request we should send next emails:
|
||||
# When create a merge request we should send an email to:
|
||||
#
|
||||
# * mr assignee if their notification level is not Disabled
|
||||
# * project team members with notification level higher then Participating
|
||||
# * watchers of the mr's labels
|
||||
#
|
||||
def new_merge_request(merge_request, current_user)
|
||||
new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
|
||||
end
|
||||
|
||||
# When we reassign a merge_request we should send next emails:
|
||||
# When we reassign a merge_request we should send an email to:
|
||||
#
|
||||
# * merge_request old assignee if their notification level is not Disabled
|
||||
# * merge_request assignee if their notification level is not Disabled
|
||||
|
@ -70,6 +80,14 @@ class NotificationService
|
|||
reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
|
||||
end
|
||||
|
||||
# When we add labels to a merge request we should send an email to:
|
||||
#
|
||||
# * watchers of the mr's labels
|
||||
#
|
||||
def relabeled_merge_request(merge_request, added_labels, current_user)
|
||||
relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
|
||||
end
|
||||
|
||||
def close_mr(merge_request, current_user)
|
||||
close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
|
||||
end
|
||||
|
@ -91,7 +109,8 @@ class NotificationService
|
|||
reopen_resource_email(
|
||||
merge_request,
|
||||
merge_request.target_project,
|
||||
current_user, 'merge_request_status_email',
|
||||
current_user,
|
||||
'merge_request_status_email',
|
||||
'reopened'
|
||||
)
|
||||
end
|
||||
|
@ -348,19 +367,23 @@ class NotificationService
|
|||
end
|
||||
|
||||
def add_subscribed_users(recipients, target)
|
||||
return recipients unless target.respond_to? :subscriptions
|
||||
return recipients unless target.respond_to? :subscribers
|
||||
|
||||
subscriptions = target.subscriptions
|
||||
recipients + target.subscribers
|
||||
end
|
||||
|
||||
if subscriptions.any?
|
||||
recipients + subscriptions.where(subscribed: true).map(&:user)
|
||||
else
|
||||
recipients
|
||||
def add_labels_subscribers(recipients, target, labels: nil)
|
||||
return recipients unless target.respond_to? :labels
|
||||
|
||||
(labels || target.labels).each do |label|
|
||||
recipients += label.subscribers
|
||||
end
|
||||
|
||||
recipients
|
||||
end
|
||||
|
||||
def new_resource_email(target, project, method)
|
||||
recipients = build_recipients(target, project, target.author)
|
||||
recipients = build_recipients(target, project, target.author, action: :new)
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.send(method, recipient.id, target.id).deliver_later
|
||||
|
@ -392,6 +415,15 @@ class NotificationService
|
|||
end
|
||||
end
|
||||
|
||||
def relabeled_resource_email(target, labels, current_user, method)
|
||||
recipients = build_relabeled_recipients(target, current_user, labels: labels)
|
||||
label_names = labels.map(&:name)
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def reopen_resource_email(target, project, current_user, method, status)
|
||||
recipients = build_recipients(target, project, current_user)
|
||||
|
||||
|
@ -416,6 +448,11 @@ class NotificationService
|
|||
|
||||
recipients = reject_muted_users(recipients, project)
|
||||
recipients = add_subscribed_users(recipients, target)
|
||||
|
||||
if action == :new
|
||||
recipients = add_labels_subscribers(recipients, target)
|
||||
end
|
||||
|
||||
recipients = reject_unsubscribed_users(recipients, target)
|
||||
|
||||
recipients.delete(current_user)
|
||||
|
@ -423,6 +460,13 @@ class NotificationService
|
|||
recipients.uniq
|
||||
end
|
||||
|
||||
def build_relabeled_recipients(target, current_user, labels:)
|
||||
recipients = add_labels_subscribers([], target, labels: labels)
|
||||
recipients = reject_unsubscribed_users(recipients, target)
|
||||
recipients.delete(current_user)
|
||||
recipients.uniq
|
||||
end
|
||||
|
||||
def mailer
|
||||
Notify
|
||||
end
|
||||
|
|
|
@ -9,12 +9,39 @@ module Projects
|
|||
class HousekeepingService < BaseService
|
||||
include Gitlab::ShellAdapter
|
||||
|
||||
LEASE_TIMEOUT = 3600
|
||||
|
||||
class LeaseTaken < StandardError
|
||||
def to_s
|
||||
"Somebody already triggered housekeeping for this project in the past #{LEASE_TIMEOUT / 60} minutes"
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(project)
|
||||
@project = project
|
||||
end
|
||||
|
||||
def execute
|
||||
raise LeaseTaken if !try_obtain_lease
|
||||
|
||||
GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
|
||||
ensure
|
||||
@project.update_column(:pushes_since_gc, 0)
|
||||
end
|
||||
|
||||
def needed?
|
||||
@project.pushes_since_gc >= 10
|
||||
end
|
||||
|
||||
def increment!
|
||||
@project.increment!(:pushes_since_gc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def try_obtain_lease
|
||||
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
|
||||
lease.try_obtain
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,22 +2,11 @@
|
|||
|
||||
class AvatarUploader < CarrierWave::Uploader::Base
|
||||
include UploaderHelper
|
||||
include CarrierWave::MiniMagick
|
||||
|
||||
storage :file
|
||||
|
||||
after :store, :reset_events_cache
|
||||
|
||||
process :cropper
|
||||
|
||||
def cropper
|
||||
return unless model.respond_to?(:avatar_crop_size) && model.valid?
|
||||
|
||||
manipulate! do |img|
|
||||
img.crop "#{model.avatar_crop_size}x#{model.avatar_crop_size}+#{model.avatar_crop_x}+#{model.avatar_crop_y}"
|
||||
end
|
||||
end
|
||||
|
||||
def store_dir
|
||||
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
|
||||
end
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
= ci_status_with_icon(build.status)
|
||||
|
||||
%td.build-link
|
||||
- if can?(current_user, :read_build, project) && build.target_url
|
||||
= link_to build.target_url do
|
||||
- if can?(current_user, :read_build, build.project)
|
||||
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do
|
||||
%strong Build ##{build.id}
|
||||
- else
|
||||
%strong Build ##{build.id}
|
||||
|
||||
- if build.show_warning?
|
||||
- if build.stuck?
|
||||
%i.fa.fa-warning.text-warning
|
||||
|
||||
%td
|
||||
|
@ -18,11 +18,11 @@
|
|||
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
|
||||
|
||||
%td
|
||||
= link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
|
||||
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
|
||||
|
||||
%td
|
||||
- if build.ref
|
||||
= link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
|
||||
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
|
||||
- else
|
||||
.light none
|
||||
|
||||
|
@ -61,13 +61,12 @@
|
|||
%td
|
||||
.pull-right
|
||||
- if can?(current_user, :read_build, project) && build.artifacts?
|
||||
= link_to build.artifacts_download_url, title: 'Download artifacts' do
|
||||
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
|
||||
%i.fa.fa-download
|
||||
- if can?(current_user, :update_build, build.project)
|
||||
- if build.active?
|
||||
- if build.cancel_url
|
||||
= link_to build.cancel_url, method: :post, title: 'Cancel' do
|
||||
%i.fa.fa-remove.cred
|
||||
- elsif defined?(allow_retry) && allow_retry && build.retry_url
|
||||
= link_to build.retry_url, method: :post, title: 'Retry' do
|
||||
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
|
||||
%i.fa.fa-remove.cred
|
||||
- elsif defined?(allow_retry) && allow_retry && build.retryable?
|
||||
= link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
|
||||
%i.fa.fa-repeat
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
%tr.build
|
||||
%td.status
|
||||
= ci_status_with_icon(commit.status)
|
||||
- if commit.running?
|
||||
·
|
||||
= commit.stage
|
||||
|
||||
|
||||
%td.build-link
|
||||
= link_to ci_status_path(commit) do
|
||||
%strong #{commit.short_sha}
|
||||
|
||||
%td.build-message
|
||||
%span= truncate_first_line(commit.git_commit_message)
|
||||
|
||||
%td.build-branch
|
||||
- unless @ref
|
||||
%span
|
||||
- commit.refs.each do |ref|
|
||||
= link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
|
||||
|
||||
%td.duration
|
||||
- if commit.duration > 0
|
||||
#{time_interval_in_words commit.duration}
|
||||
|
||||
%td.timestamp
|
||||
- if commit.finished_at
|
||||
%span #{time_ago_in_words commit.finished_at} ago
|
||||
|
||||
- if commit.coverage
|
||||
%td.coverage
|
||||
#{commit.coverage}%
|
|
@ -1,4 +1,10 @@
|
|||
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
|
||||
= form_tag oauth_application_path(application) do
|
||||
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
|
||||
- if defined? small
|
||||
= button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
|
||||
%span.sr-only
|
||||
Destroy
|
||||
= icon('trash')
|
||||
- else
|
||||
= submit_tag 'Destroy', data: { confirm: "Are you sure?" }, class: submit_btn_css
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
|
||||
= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
|
||||
- if application.errors.any?
|
||||
.alert.alert-danger
|
||||
%ul
|
||||
|
@ -6,25 +6,20 @@
|
|||
%li= msg
|
||||
|
||||
.form-group
|
||||
= f.label :name, class: 'control-label'
|
||||
|
||||
.col-sm-10
|
||||
= f.text_field :name, class: 'form-control', required: true
|
||||
= f.label :name, class: 'label-light'
|
||||
= f.text_field :name, class: 'form-control', required: true
|
||||
|
||||
.form-group
|
||||
= f.label :redirect_uri, class: 'control-label'
|
||||
|
||||
.col-sm-10
|
||||
= f.text_area :redirect_uri, class: 'form-control', required: true
|
||||
= f.label :redirect_uri, class: 'label-light'
|
||||
= f.text_area :redirect_uri, class: 'form-control', required: true
|
||||
|
||||
%span.help-block
|
||||
Use one line per URI
|
||||
- if Doorkeeper.configuration.native_redirect_uri
|
||||
%span.help-block
|
||||
Use one line per URI
|
||||
- if Doorkeeper.configuration.native_redirect_uri
|
||||
%span.help-block
|
||||
Use
|
||||
%code= Doorkeeper.configuration.native_redirect_uri
|
||||
for local tests
|
||||
Use
|
||||
%code= Doorkeeper.configuration.native_redirect_uri
|
||||
for local tests
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Submit', class: "btn btn-create"
|
||||
= link_to "Cancel", applications_profile_path, class: "btn btn-cancel"
|
||||
.prepend-top-default
|
||||
= f.submit 'Save application', class: "btn btn-create"
|
||||
|
|
|
@ -1,19 +1,83 @@
|
|||
- page_title "Applications"
|
||||
%h3.page-title Your applications
|
||||
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||
- header_title page_title, applications_profile_path
|
||||
|
||||
.table-holder
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td= application.redirect_uri
|
||||
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
|
||||
%td= render 'delete_form', application: application
|
||||
.row.prepend-top-default
|
||||
.col-lg-3.profile-settings-sidebar
|
||||
%h4.prepend-top-0
|
||||
= page_title
|
||||
%p
|
||||
- if user_oauth_applications?
|
||||
Manage applications that can use GitLab as an OAuth provider,
|
||||
and applications that you've authorized to use your account.
|
||||
- else
|
||||
Manage applications that you've authorized to use your account.
|
||||
.col-lg-9
|
||||
- if user_oauth_applications?
|
||||
%h5.prepend-top-0
|
||||
Add new application
|
||||
= render 'form', application: @application
|
||||
%hr
|
||||
- if user_oauth_applications?
|
||||
.oauth-applications
|
||||
%h5
|
||||
Your applications (#{@applications.size})
|
||||
- if @applications.any?
|
||||
.table-responsive
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th Clients
|
||||
%th.last-heading
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{id: "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td
|
||||
- application.redirect_uri.split.each do |uri|
|
||||
%div= uri
|
||||
%td= application.access_tokens.count
|
||||
%td
|
||||
= link_to edit_oauth_application_path(application), class: "btn btn-transparent append-right-5" do
|
||||
%span.sr-only
|
||||
Edit
|
||||
= icon('pencil')
|
||||
= render 'delete_form', application: application, small: true
|
||||
- else
|
||||
.profile-settings-message.text-center
|
||||
You don't have any applications
|
||||
.oauth-authorized-applications.prepend-top-20.append-bottom-default
|
||||
- if user_oauth_applications?
|
||||
%h5
|
||||
Authorized applications (#{@authorized_tokens.size})
|
||||
|
||||
- if @authorized_tokens.any?
|
||||
.table-responsive
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Authorized At
|
||||
%th Scope
|
||||
%th
|
||||
%tbody
|
||||
- @authorized_apps.each do |app|
|
||||
- token = app.authorized_tokens.order('created_at desc').first
|
||||
%tr{id: "application_#{app.id}"}
|
||||
%td= app.name
|
||||
%td= token.created_at
|
||||
%td= token.scopes
|
||||
%td= render 'delete_form', application: app
|
||||
- @authorized_anonymous_tokens.each do |token|
|
||||
%tr
|
||||
%td
|
||||
Anonymous
|
||||
%div.help-block
|
||||
%em Authorization was granted by entering your username and password in the application.
|
||||
%td= token.created_at
|
||||
%td= token.scopes
|
||||
%td= render 'delete_form', token: token
|
||||
- else
|
||||
.profile-settings-message.text-center
|
||||
You don't have any authorized applications
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%li.commit
|
||||
.commit-row-title
|
||||
= link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
|
||||
= link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
|
||||
·
|
||||
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.event-last-push
|
||||
.event-last-push-text
|
||||
%span You pushed to
|
||||
= link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
|
||||
= link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
|
||||
%strong= event.ref_name
|
||||
%span at
|
||||
%strong= link_to_project event.project
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
%strong= event.ref_name
|
||||
- else
|
||||
%strong
|
||||
= link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
|
||||
= link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.target_title)
|
||||
at
|
||||
= link_to_project event.project
|
||||
|
||||
|
|
|
@ -1,12 +1 @@
|
|||
.top-area
|
||||
.nav-controls
|
||||
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
|
||||
- if @projects.present?
|
||||
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
|
||||
= render 'shared/projects/dropdown'
|
||||
- if can? current_user, :create_projects, @group
|
||||
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
|
||||
= icon('plus')
|
||||
New Project
|
||||
|
||||
= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
|
||||
= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
|
||||
|
|
|
@ -1,18 +1 @@
|
|||
- if projects.present?
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Projects shared with
|
||||
%strong #{@group.name}
|
||||
(#{projects.count})
|
||||
%ul.well-list
|
||||
- projects.each do |project|
|
||||
%li.project-row
|
||||
= link_to namespace_project_path(project.namespace, project), class: dom_class(project) do
|
||||
%span.namespace-name
|
||||
- if project.namespace
|
||||
= project.namespace.human_name
|
||||
\/
|
||||
%span.project-name
|
||||
= truncate(project.name, length: 25)
|
||||
%span.arrow
|
||||
%i.icon-angle-right
|
||||
= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
|
||||
|
|
|
@ -27,24 +27,33 @@
|
|||
.cover-desc.description
|
||||
= markdown(@group.description, pipeline: :description)
|
||||
|
||||
|
||||
%ul.nav-links
|
||||
%li.active
|
||||
= link_to "#projects", 'data-toggle' => 'tab' do
|
||||
Projects
|
||||
- if @shared_projects.present?
|
||||
%li
|
||||
= link_to "#shared", 'data-toggle' => 'tab' do
|
||||
Shared Projects
|
||||
|
||||
- if can?(current_user, :read_group, @group)
|
||||
%div{ class: container_class }
|
||||
.top-area
|
||||
%ul.nav-links
|
||||
%li.active
|
||||
= link_to "#projects", 'data-toggle' => 'tab' do
|
||||
All Projects
|
||||
- if @shared_projects.present?
|
||||
%li
|
||||
= link_to "#shared", 'data-toggle' => 'tab' do
|
||||
Shared Projects
|
||||
.nav-controls
|
||||
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
|
||||
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
|
||||
= render 'shared/projects/dropdown'
|
||||
- if can? current_user, :create_projects, @group
|
||||
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
|
||||
= icon('plus')
|
||||
New Project
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active#projects
|
||||
= render "projects", projects: @projects
|
||||
|
||||
.tab-pane#shared
|
||||
= render "shared_projects", projects: @shared_projects
|
||||
- if @shared_projects.present?
|
||||
.tab-pane#shared
|
||||
= render "shared_projects", projects: @shared_projects
|
||||
|
||||
- else
|
||||
%p.nav-links.no-top
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
= icon('gear fw')
|
||||
%span
|
||||
Account
|
||||
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
|
||||
= nav_link(controller: 'oauth/applications') do
|
||||
= link_to applications_profile_path, title: 'Applications' do
|
||||
= icon('cloud fw')
|
||||
%span
|
||||
|
|
|
@ -42,12 +42,15 @@
|
|||
- else
|
||||
#{link_to "View it on GitLab", @target_url}.
|
||||
%br
|
||||
-# Don't link the host is the line below, one link in the email is easier to quickly click than two.
|
||||
-# Don't link the host in the line below, one link in the email is easier to quickly click than two.
|
||||
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
|
||||
If you'd like to receive fewer emails, you can
|
||||
- if @sent_notification && @sent_notification.unsubscribable?
|
||||
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
|
||||
from this thread or
|
||||
adjust your notification settings.
|
||||
- if @labels_url
|
||||
adjust your #{link_to 'label subscriptions', @labels_url}.
|
||||
- else
|
||||
- if @sent_notification && @sent_notification.unsubscribable?
|
||||
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
|
||||
from this thread or
|
||||
adjust your notification settings.
|
||||
|
||||
= email_action @target_url
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
|
||||
|
||||
<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %>
|
||||
<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
|
||||
|
||||
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
|
||||
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
%p
|
||||
#{'Label'.pluralize(@label_names.size)} added:
|
||||
%em= @label_names.to_sentence
|
|
@ -0,0 +1,3 @@
|
|||
<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
|
||||
|
||||
<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
|
|
@ -0,0 +1 @@
|
|||
= render 'relabeled_issuable_email', issuable: @issue
|
|
@ -0,0 +1 @@
|
|||
<%= render 'relabeled_issuable_email', issuable: @issue %>
|
|
@ -0,0 +1 @@
|
|||
= render 'relabeled_issuable_email', issuable: @merge_request
|
|
@ -0,0 +1 @@
|
|||
<%= render 'relabeled_issuable_email', issuable: @merge_request %>
|
|
@ -1,70 +0,0 @@
|
|||
- page_title "Applications"
|
||||
- header_title page_title, applications_profile_path
|
||||
|
||||
.alert.alert-help.prepend-top-default
|
||||
- if user_oauth_applications?
|
||||
Manage applications that can use GitLab as an OAuth provider,
|
||||
and applications that you've authorized to use your account.
|
||||
- else
|
||||
Manage applications that you've authorized to use your account.
|
||||
|
||||
- if user_oauth_applications?
|
||||
.oauth-applications
|
||||
%h3
|
||||
Your applications
|
||||
.pull-right
|
||||
= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||
- if @applications.any?
|
||||
.table-holder
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th Clients
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td
|
||||
- application.redirect_uri.split.each do |uri|
|
||||
%div= uri
|
||||
%td= application.access_tokens.count
|
||||
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
|
||||
%td= render 'doorkeeper/applications/delete_form', application: application
|
||||
|
||||
.oauth-authorized-applications.prepend-top-20
|
||||
- if user_oauth_applications?
|
||||
%h3
|
||||
Authorized applications
|
||||
|
||||
- if @authorized_tokens.any?
|
||||
.table-holder
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Authorized At
|
||||
%th Scope
|
||||
%th
|
||||
%tbody
|
||||
- @authorized_apps.each do |app|
|
||||
- token = app.authorized_tokens.order('created_at desc').first
|
||||
%tr{:id => "application_#{app.id}"}
|
||||
%td= app.name
|
||||
%td= token.created_at
|
||||
%td= token.scopes
|
||||
%td= render 'doorkeeper/authorized_applications/delete_form', application: app
|
||||
- @authorized_anonymous_tokens.each do |token|
|
||||
%tr
|
||||
%td
|
||||
Anonymous
|
||||
%div.help-block
|
||||
%em Authorization was granted by entering your username and password in the application.
|
||||
%td= token.created_at
|
||||
%td= token.scopes
|
||||
%td= render 'doorkeeper/authorized_applications/delete_form', token: token
|
||||
- else
|
||||
%p.light You don't have any authorized applications
|
|
@ -1,7 +1,4 @@
|
|||
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
|
||||
= f.hidden_field :avatar_crop_x
|
||||
= f.hidden_field :avatar_crop_y
|
||||
= f.hidden_field :avatar_crop_size
|
||||
-if @user.errors.any?
|
||||
%div.alert.alert-danger
|
||||
%ul
|
||||
|
@ -97,19 +94,3 @@
|
|||
.prepend-top-default.append-bottom-default
|
||||
= f.submit 'Update profile settings', class: "btn btn-success"
|
||||
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
|
||||
|
||||
.modal.modal-profile-crop
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
%button.close{type: 'button', data: {dismiss: 'modal'}}
|
||||
%span
|
||||
×
|
||||
%h4.modal-title
|
||||
Crop your new profile picture
|
||||
.modal-body
|
||||
%p
|
||||
%img.modal-profile-crop-image
|
||||
.modal-footer
|
||||
%button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
|
||||
Set new profile picture
|
||||
|
|
|
@ -55,7 +55,6 @@
|
|||
%th Coverage
|
||||
%th
|
||||
|
||||
- @builds.each do |build|
|
||||
= render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true
|
||||
= render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
|
||||
|
||||
= paginate @builds, theme: 'gitlab'
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
|
||||
|
||||
#up-build-trace
|
||||
- if @commit.matrix_for_ref?(@build.ref)
|
||||
- builds = @build.commit.matrix_builds(@build)
|
||||
- if builds.size > 1
|
||||
%ul.nav-links.no-top.no-bottom
|
||||
- @commit.latest_builds_for_ref(@build.ref).each do |build|
|
||||
- builds.each do |build|
|
||||
%li{class: ('active' if build == @build) }
|
||||
= link_to namespace_project_build_path(@project.namespace, @project, build) do
|
||||
= ci_icon_for_status(build.status)
|
||||
|
@ -44,7 +45,7 @@
|
|||
.pull-right
|
||||
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
|
||||
|
||||
- if @build.show_warning?
|
||||
- if @build.stuck?
|
||||
- unless @build.any_runners_online?
|
||||
.bs-callout.bs-callout-warning
|
||||
%p
|
||||
|
@ -100,12 +101,12 @@
|
|||
%h4.title Build artifacts
|
||||
.center
|
||||
.btn-group{ role: :group }
|
||||
= link_to @build.artifacts_download_url, class: 'btn btn-sm btn-primary' do
|
||||
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
|
||||
= icon('download')
|
||||
Download
|
||||
|
||||
- if @build.artifacts_metadata?
|
||||
= link_to @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' do
|
||||
= link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
|
||||
= icon('folder-open')
|
||||
Browse
|
||||
|
||||
|
@ -115,10 +116,10 @@
|
|||
- if can?(current_user, :update_build, @project)
|
||||
.center
|
||||
.btn-group{ role: :group }
|
||||
- if @build.cancel_url
|
||||
= link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
|
||||
- elsif @build.retry_url
|
||||
= link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
|
||||
- if @build.active?
|
||||
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
|
||||
- elsif @build.retryable?
|
||||
= link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
|
||||
|
||||
- if @build.erasable?
|
||||
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
%tr.build
|
||||
%td.status
|
||||
- if can?(current_user, :read_build, build)
|
||||
= ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
|
||||
- else
|
||||
= ci_status_with_icon(build.status)
|
||||
|
||||
%td.build-link
|
||||
- if can?(current_user, :read_build, build)
|
||||
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do
|
||||
%strong ##{build.id}
|
||||
- else
|
||||
%strong ##{build.id}
|
||||
|
||||
- if build.stuck?
|
||||
%i.fa.fa-warning.text-warning
|
||||
|
||||
- if defined?(commit_sha) && commit_sha
|
||||
%td
|
||||
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
|
||||
|
||||
%td
|
||||
- if build.ref
|
||||
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
|
||||
- else
|
||||
.light none
|
||||
|
||||
- if defined?(runner) && runner
|
||||
%td
|
||||
- if build.try(:runner)
|
||||
= runner_link(build.runner)
|
||||
- else
|
||||
.light none
|
||||
|
||||
- if defined?(stage) && stage
|
||||
%td
|
||||
= build.stage
|
||||
|
||||
%td
|
||||
= build.name
|
||||
|
||||
.pull-right
|
||||
- if build.tags.any?
|
||||
- build.tags.each do |tag|
|
||||
%span.label.label-primary
|
||||
= tag
|
||||
- if build.try(:trigger_request)
|
||||
%span.label.label-info triggered
|
||||
- if build.try(:allow_failure)
|
||||
%span.label.label-danger allowed to fail
|
||||
|
||||
%td.duration
|
||||
- if build.duration
|
||||
#{duration_in_words(build.finished_at, build.started_at)}
|
||||
|
||||
%td.timestamp
|
||||
- if build.finished_at
|
||||
%span #{time_ago_with_tooltip(build.finished_at)}
|
||||
|
||||
- if defined?(coverage) && coverage
|
||||
%td.coverage
|
||||
- if build.try(:coverage)
|
||||
#{build.coverage}%
|
||||
|
||||
%td
|
||||
.pull-right
|
||||
- if can?(current_user, :read_build, build) && build.artifacts?
|
||||
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
|
||||
%i.fa.fa-download
|
||||
- if can?(current_user, :update_build, build)
|
||||
- if build.active?
|
||||
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
|
||||
%i.fa.fa-remove.cred
|
||||
- elsif defined?(allow_retry) && allow_retry && build.retryable?
|
||||
= link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
|
||||
%i.fa.fa-repeat
|
|
@ -43,8 +43,8 @@
|
|||
%th Coverage
|
||||
%th
|
||||
- @ci_commit.refs.each do |ref|
|
||||
= render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
|
||||
locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
|
||||
- builds = @ci_commit.statuses.for_ref(ref).latest.ordered
|
||||
= render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
|
||||
|
||||
- if @ci_commit.retried.any?
|
||||
.gray-content-block.second-block
|
||||
|
@ -64,5 +64,4 @@
|
|||
- if @ci_commit.project.build_coverage_enabled?
|
||||
%th Coverage
|
||||
%th
|
||||
= render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
|
||||
locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
|
||||
= render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
%tr.commit_status
|
||||
%td.status
|
||||
- if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
|
||||
= link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
|
||||
= ci_icon_for_status(commit_status.status)
|
||||
= commit_status.status
|
||||
- else
|
||||
= ci_status_with_icon(commit_status.status)
|
||||
|
||||
%td.commit_status-link
|
||||
- if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
|
||||
= link_to commit_status.target_url do
|
||||
%strong ##{commit_status.id}
|
||||
- else
|
||||
%strong ##{commit_status.id}
|
||||
|
||||
- if commit_status.show_warning?
|
||||
%i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"}
|
||||
|
||||
- if defined?(commit_sha) && commit_sha
|
||||
%td
|
||||
= link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
|
||||
|
||||
%td
|
||||
- if commit_status.ref
|
||||
= link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
|
||||
- else
|
||||
.light none
|
||||
|
||||
- if defined?(runner) && runner
|
||||
%td
|
||||
- if commit_status.try(:runner)
|
||||
= runner_link(commit_status.runner)
|
||||
- else
|
||||
.light none
|
||||
|
||||
- if defined?(stage) && stage
|
||||
%td
|
||||
= commit_status.stage
|
||||
|
||||
%td
|
||||
= commit_status.name
|
||||
|
||||
.pull-right
|
||||
- if commit_status.tags.any?
|
||||
- commit_status.tags.each do |tag|
|
||||
%span.label.label-primary
|
||||
= tag
|
||||
- if commit_status.try(:trigger_request)
|
||||
%span.label.label-info triggered
|
||||
- if commit_status.try(:allow_failure)
|
||||
%span.label.label-danger allowed to fail
|
||||
|
||||
%td.duration
|
||||
- if commit_status.duration
|
||||
#{duration_in_words(commit_status.finished_at, commit_status.started_at)}
|
||||
|
||||
%td.timestamp
|
||||
- if commit_status.finished_at
|
||||
%span #{time_ago_with_tooltip(commit_status.finished_at)}
|
||||
|
||||
- if defined?(coverage) && coverage
|
||||
%td.coverage
|
||||
- if commit_status.try(:coverage)
|
||||
#{commit_status.coverage}%
|
||||
|
||||
%td
|
||||
.pull-right
|
||||
- if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url
|
||||
= link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
|
||||
%i.fa.fa-download
|
||||
- if can?(current_user, :update_commit_status, commit_status)
|
||||
- if commit_status.active?
|
||||
- if commit_status.cancel_url
|
||||
= link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
|
||||
%i.fa.fa-remove.cred
|
||||
- elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
|
||||
= link_to commit_status.retry_url, method: :post, title: 'Retry' do
|
||||
%i.fa.fa-repeat
|
|
@ -0,0 +1,58 @@
|
|||
%tr.generic_commit_status
|
||||
%td.status
|
||||
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
|
||||
= ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
|
||||
- else
|
||||
= ci_status_with_icon(generic_commit_status.status)
|
||||
|
||||
%td.generic_commit_status-link
|
||||
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
|
||||
= link_to generic_commit_status.target_url do
|
||||
%strong ##{generic_commit_status.id}
|
||||
- else
|
||||
%strong ##{generic_commit_status.id}
|
||||
|
||||
- if defined?(commit_sha) && commit_sha
|
||||
%td
|
||||
= link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
|
||||
|
||||
%td
|
||||
- if generic_commit_status.ref
|
||||
= link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
|
||||
- else
|
||||
.light none
|
||||
|
||||
- if defined?(runner) && runner
|
||||
%td
|
||||
- if generic_commit_status.try(:runner)
|
||||
= runner_link(generic_commit_status.runner)
|
||||
- else
|
||||
.light none
|
||||
|
||||
- if defined?(stage) && stage
|
||||
%td
|
||||
= generic_commit_status.stage
|
||||
|
||||
%td
|
||||
= generic_commit_status.name
|
||||
|
||||
.pull-right
|
||||
- if generic_commit_status.tags.any?
|
||||
- generic_commit_status.tags.each do |tag|
|
||||
%span.label.label-primary
|
||||
= tag
|
||||
|
||||
%td.duration
|
||||
- if generic_commit_status.duration
|
||||
#{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
|
||||
|
||||
%td.timestamp
|
||||
- if generic_commit_status.finished_at
|
||||
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
|
||||
|
||||
- if defined?(coverage) && coverage
|
||||
%td.coverage
|
||||
- if generic_commit_status.try(:coverage)
|
||||
#{generic_commit_status.coverage}%
|
||||
|
||||
%td
|
|
@ -10,6 +10,16 @@
|
|||
= link_to_label(label) do
|
||||
= pluralize label.open_issues_count, 'open issue'
|
||||
|
||||
- if current_user
|
||||
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
|
||||
.subscription-status{data: {status: label_subscription_status(label)}}
|
||||
%button.btn.btn-sm.btn-info.subscribe-button
|
||||
%span= label_subscription_toggle_button_text(label)
|
||||
|
||||
- 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 '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?"}
|
||||
|
||||
- if current_user
|
||||
:javascript
|
||||
new Subscription('##{dom_id(label)} .label-subscription');
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
%a{href: "#", data: {id: "close"}} Closed
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
|
||||
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
%hr
|
||||
- if current_user
|
||||
- subscribed = issuable.subscribed?(current_user)
|
||||
.block.light
|
||||
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
|
||||
.sidebar-collapsed-icon
|
||||
= icon('rss')
|
||||
.title.hide-collapsed
|
||||
|
@ -124,5 +124,5 @@
|
|||
= clipboard_button(clipboard_text: project_ref)
|
||||
|
||||
:javascript
|
||||
new Subscription("#{toggle_subscription_path(issuable)}");
|
||||
new Subscription('.subscription');
|
||||
new IssuableContext();
|
||||
|
|
|
@ -295,7 +295,7 @@ Rails.application.routes.draw do
|
|||
resource :profile, only: [:show, :update] do
|
||||
member do
|
||||
get :audit_log
|
||||
get :applications
|
||||
get :applications, to: 'oauth/applications#index'
|
||||
|
||||
put :reset_private_token
|
||||
put :update_username
|
||||
|
@ -675,6 +675,10 @@ Rails.application.routes.draw do
|
|||
collection do
|
||||
post :generate
|
||||
end
|
||||
|
||||
member do
|
||||
post :toggle_subscription
|
||||
end
|
||||
end
|
||||
|
||||
resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class ProjectsAddPushesSinceGc < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :projects, :pushes_since_gc, :integer, default: 0
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20160310185910) do
|
||||
ActiveRecord::Schema.define(version: 20160314143402) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -720,6 +720,7 @@ ActiveRecord::Schema.define(version: 20160310185910) do
|
|||
t.boolean "pending_delete", default: false
|
||||
t.boolean "public_builds", default: true, null: false
|
||||
t.string "main_language"
|
||||
t.integer "pushes_since_gc", default: 0
|
||||
end
|
||||
|
||||
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
|
||||
|
|
|
@ -33,7 +33,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2015-12-24T15:51:21.802Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": {
|
||||
"filename": "artifacts.zip",
|
||||
"size": 1000
|
||||
|
@ -75,7 +74,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2015-12-24T15:51:21.727Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": null,
|
||||
"finished_at": "2015-12-24T17:54:24.921Z",
|
||||
"id": 6,
|
||||
|
@ -139,7 +137,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2016-01-11T10:13:33.506Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": null,
|
||||
"finished_at": "2016-01-11T10:14:09.526Z",
|
||||
"id": 69,
|
||||
|
@ -164,7 +161,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2015-12-24T15:51:21.957Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": null,
|
||||
"finished_at": "2015-12-24T17:54:33.913Z",
|
||||
"id": 9,
|
||||
|
@ -226,7 +222,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2015-12-24T15:51:21.880Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": null,
|
||||
"finished_at": "2015-12-24T17:54:31.198Z",
|
||||
"id": 8,
|
||||
|
@ -315,7 +310,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2016-01-11T10:13:33.506Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": null,
|
||||
"finished_at": "2016-01-11T10:14:09.526Z",
|
||||
"id": 69,
|
||||
|
@ -362,7 +356,6 @@ Example of response
|
|||
},
|
||||
"coverage": null,
|
||||
"created_at": "2016-01-11T10:13:33.506Z",
|
||||
"download_url": null,
|
||||
"artifacts_file": null,
|
||||
"finished_at": null,
|
||||
"id": 69,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
## Test and Deploy a ruby application
|
||||
This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application.
|
||||
This example will guide you how to run tests in your Ruby application and deploy it automatically as Heroku application.
|
||||
|
||||
You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
|
||||
|
||||
|
|
|
@ -116,7 +116,8 @@ Alias for [stages](#stages).
|
|||
|
||||
### variables
|
||||
|
||||
_**Note:** Introduced in GitLab Runner v0.5.0._
|
||||
>**Note:**
|
||||
Introduced in GitLab Runner v0.5.0.
|
||||
|
||||
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
|
||||
environment. The variables are stored in the git repository and are meant to
|
||||
|
@ -153,7 +154,8 @@ cache:
|
|||
|
||||
#### cache:key
|
||||
|
||||
_**Note:** Introduced in GitLab Runner v1.0.0._
|
||||
>**Note:**
|
||||
Introduced in GitLab Runner v1.0.0.
|
||||
|
||||
The `key` directive allows you to define the affinity of caching
|
||||
between jobs, allowing to have a single cache for all jobs,
|
||||
|
@ -234,13 +236,14 @@ job_name:
|
|||
| Keyword | Required | Description |
|
||||
|---------------|----------|-------------|
|
||||
| script | yes | Defines a shell script which is executed by runner |
|
||||
| stage | no (default: `test`) | Defines a build stage |
|
||||
| stage | no | Defines a build stage (default: `test`) |
|
||||
| type | no | Alias for `stage` |
|
||||
| only | no | Defines a list of git refs for which build is created |
|
||||
| except | no | Defines a list of git refs for which build is not created |
|
||||
| tags | no | Defines a list of tags which are used to select runner |
|
||||
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
|
||||
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
|
||||
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
|
||||
| artifacts | no | Define list build artifacts |
|
||||
| cache | no | Define list of files that should be cached between subsequent runs |
|
||||
|
||||
|
@ -393,15 +396,18 @@ The above script will:
|
|||
|
||||
### artifacts
|
||||
|
||||
_**Note:** Introduced in GitLab Runner v0.7.0 for non-Windows platforms._
|
||||
|
||||
_**Note:** Limited Windows support was added in GitLab Runner v.1.0.0.
|
||||
Currently not all executors are supported._
|
||||
|
||||
_**Note:** Build artifacts are only collected for successful builds._
|
||||
>**Notes:**
|
||||
>
|
||||
> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
|
||||
> - Windows support was added in GitLab Runner v.1.0.0.
|
||||
> - Currently not all executors are supported.
|
||||
> - Build artifacts are only collected for successful builds.
|
||||
|
||||
`artifacts` is used to specify list of files and directories which should be
|
||||
attached to build after success. Below are some examples.
|
||||
attached to build after success. To pass artifacts between different builds,
|
||||
see [dependencies](#dependencies).
|
||||
|
||||
Below are some examples.
|
||||
|
||||
Send all files in `binaries` and `.config`:
|
||||
|
||||
|
@ -453,9 +459,130 @@ release-job:
|
|||
The artifacts will be sent to GitLab after a successful build and will
|
||||
be available for download in the GitLab UI.
|
||||
|
||||
#### artifacts:name
|
||||
|
||||
>**Note:**
|
||||
Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
|
||||
|
||||
The `name` directive allows you to define the name of the created artifacts
|
||||
archive. That way, you can have a unique name of every archive which could be
|
||||
useful when you'd like to download the archive from GitLab. The `artifacts:name`
|
||||
variable can make use of any of the [predefined variables](../variables/README.md).
|
||||
|
||||
---
|
||||
|
||||
**Example configurations**
|
||||
|
||||
To create an archive with a name of the current build:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "$CI_BUILD_NAME"
|
||||
```
|
||||
|
||||
To create an archive with a name of the current branch or tag including only
|
||||
the files that are untracked by Git:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "$CI_BUILD_REF_NAME"
|
||||
untracked: true
|
||||
```
|
||||
|
||||
To create an archive with a name of the current build and the current branch or
|
||||
tag including only the files that are untracked by Git:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
|
||||
untracked: true
|
||||
```
|
||||
|
||||
To create an archive with a name of the current [stage](#stages) and branch name:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
|
||||
untracked: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you use **Windows Batch** to run your shell scripts you need to replace
|
||||
`$` with `%`:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
|
||||
untracked: true
|
||||
```
|
||||
|
||||
### dependencies
|
||||
|
||||
>**Note:**
|
||||
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
|
||||
|
||||
This feature should be used in conjunction with [`artifacts`](#artifacts) and
|
||||
allows you to define the artifacts to pass between different builds.
|
||||
|
||||
Note that `artifacts` from previous [stages](#stages) are passed by default.
|
||||
|
||||
To use this feature, define `dependencies` in context of the job and pass
|
||||
a list of all previous builds from which the artifacts should be downloaded.
|
||||
You can only define builds from stages that are executed before the current one.
|
||||
An error will be shown if you define builds from the current stage or next ones.
|
||||
|
||||
---
|
||||
|
||||
In the following example, we define two jobs with artifacts, `build:osx` and
|
||||
`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
|
||||
will be downloaded and extracted in the context of the build. The same happens
|
||||
for `test:linux` and artifacts from `build:linux`.
|
||||
|
||||
The job `deploy` will download artifacts from all previous builds because of
|
||||
the [stage](#stages) precedence:
|
||||
|
||||
```yaml
|
||||
build:osx:
|
||||
stage: build
|
||||
script: make build:osx
|
||||
artifacts:
|
||||
paths:
|
||||
- binaries/
|
||||
|
||||
build:linux:
|
||||
stage: build
|
||||
script: make build:linux
|
||||
artifacts:
|
||||
paths:
|
||||
- binaries/
|
||||
|
||||
test:osx:
|
||||
stage: test
|
||||
script: make test:osx
|
||||
dependencies:
|
||||
- build:osx
|
||||
|
||||
test:linux:
|
||||
stage: test
|
||||
script: make test:linux
|
||||
dependencies:
|
||||
- build:linux
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
script: make deploy
|
||||
```
|
||||
|
||||
### cache
|
||||
|
||||
_**Note:** Introduced in GitLab Runner v0.7.0._
|
||||
>**Note:**
|
||||
Introduced in GitLab Runner v0.7.0.
|
||||
|
||||
`cache` is used to specify list of files and directories which should be cached
|
||||
between builds. Below are some examples:
|
||||
|
@ -509,6 +636,155 @@ rspec:
|
|||
The cache is provided on best effort basis, so don't expect that cache will be
|
||||
always present. For implementation details please check GitLab Runner.
|
||||
|
||||
## Hidden jobs
|
||||
|
||||
>**Note:**
|
||||
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
|
||||
|
||||
Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
|
||||
use this feature to ignore jobs, or use the
|
||||
[special YAML features](#special-yaml-features) and transform the hidden jobs
|
||||
into templates.
|
||||
|
||||
In the following example, `.job_name` will be ignored:
|
||||
|
||||
```yaml
|
||||
.job_name:
|
||||
script:
|
||||
- rake spec
|
||||
```
|
||||
|
||||
## Special YAML features
|
||||
|
||||
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
|
||||
and map merging (`<<`), which will allow you to greatly reduce the complexity
|
||||
of `.gitlab-ci.yml`.
|
||||
|
||||
Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
|
||||
|
||||
### Anchors
|
||||
|
||||
>**Note:**
|
||||
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
|
||||
|
||||
YAML also has a handy feature called 'anchors', which let you easily duplicate
|
||||
content across your document. Anchors can be used to duplicate/inherit
|
||||
properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
|
||||
to provide templates for your jobs.
|
||||
|
||||
The following example uses anchors and map merging. It will create two jobs,
|
||||
`test1` and `test2`, that will inherit the parameters of `.job_template`, each
|
||||
having their own custom `script` defined:
|
||||
|
||||
```yaml
|
||||
.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
|
||||
image: ruby:2.1
|
||||
services:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
test1:
|
||||
<<: *job_definition # Merge the contents of the 'job_definition' alias
|
||||
script:
|
||||
- test1 project
|
||||
|
||||
test2:
|
||||
<<: *job_definition # Merge the contents of the 'job_definition' alias
|
||||
script:
|
||||
- test2 project
|
||||
```
|
||||
|
||||
`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
|
||||
given hash into the current one", and `*` includes the named anchor
|
||||
(`job_definition` again). The expanded version looks like this:
|
||||
|
||||
```yaml
|
||||
.job_template:
|
||||
image: ruby:2.1
|
||||
services:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
test1:
|
||||
image: ruby:2.1
|
||||
services:
|
||||
- postgres
|
||||
- redis
|
||||
script:
|
||||
- test1 project
|
||||
|
||||
test2:
|
||||
image: ruby:2.1
|
||||
services:
|
||||
- postgres
|
||||
- redis
|
||||
script:
|
||||
- test2 project
|
||||
```
|
||||
|
||||
Let's see another one example. This time we will use anchors to define two sets
|
||||
of services. This will create two jobs, `test:postgres` and `test:mysql`, that
|
||||
will share the `script` directive defined in `.job_template`, and the `services`
|
||||
directive defined in `.postgres_services` and `.mysql_services` respectively:
|
||||
|
||||
```yaml
|
||||
.job_template: &job_definition
|
||||
script:
|
||||
- test project
|
||||
|
||||
.postgres_services:
|
||||
services: &postgres_definition
|
||||
- postgres
|
||||
- ruby
|
||||
|
||||
.mysql_services:
|
||||
services: &mysql_definition
|
||||
- mysql
|
||||
- ruby
|
||||
|
||||
test:postgres:
|
||||
<< *job_definition
|
||||
services: *postgres_definition
|
||||
|
||||
test:mysql:
|
||||
<< *job_definition
|
||||
services: *mysql_definition
|
||||
```
|
||||
|
||||
The expanded version looks like this:
|
||||
|
||||
```yaml
|
||||
.job_template:
|
||||
script:
|
||||
- test project
|
||||
|
||||
.postgres_services:
|
||||
services:
|
||||
- postgres
|
||||
- ruby
|
||||
|
||||
.mysql_services:
|
||||
services:
|
||||
- mysql
|
||||
- ruby
|
||||
|
||||
test:postgres:
|
||||
script:
|
||||
- test project
|
||||
services:
|
||||
- postgres
|
||||
- ruby
|
||||
|
||||
test:mysql:
|
||||
script:
|
||||
- test project
|
||||
services:
|
||||
- mysql
|
||||
- ruby
|
||||
```
|
||||
|
||||
You can see that the hidden jobs are conveniently used as templates.
|
||||
|
||||
## Validate the .gitlab-ci.yml
|
||||
|
||||
Each instance of GitLab CI has an embedded debug tool called Lint.
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
# SCSS styleguide
|
||||
|
||||
This style guide recommends best practices for SCSS to make styles easy to read,
|
||||
easy to maintain, and performant for the end-user.
|
||||
|
||||
## Rules
|
||||
|
||||
### Naming
|
||||
|
||||
CSS classes should use the `lowercase-hyphenated` format rather than
|
||||
`snake_case` or `camelCase`.
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
.class_name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// Bad
|
||||
.className {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// Good
|
||||
.class-name {
|
||||
color: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
You should always use a space before a brace, braces should be on the same
|
||||
line, each property should each get its own line, and there should be a space
|
||||
between the property and its value.
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
.container-item {
|
||||
width: 100px; height: 100px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// Bad
|
||||
.container-item
|
||||
{
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// Bad
|
||||
.container-item{
|
||||
width:100px;
|
||||
height:100px;
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
// Good
|
||||
.container-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Note that there is an exception for single-line rulesets, although these are
|
||||
not typically recommended.
|
||||
|
||||
```scss
|
||||
p { margin: 0; padding: 0; }
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
HEX (hexadecimal) colors short-form should use shortform where possible, and
|
||||
should use lower case letters to differenciate between letters and numbers, e.
|
||||
g. `#E3E3E3` vs. `#e3e3e3`.
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
// Bad
|
||||
p {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
// Good
|
||||
p {
|
||||
color: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
### Indentation
|
||||
|
||||
Indentation should always use two spaces for each indentation level.
|
||||
|
||||
```scss
|
||||
// Bad, four spaces
|
||||
p {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
// Good
|
||||
p {
|
||||
color: #f00;
|
||||
}
|
||||
```
|
||||
|
||||
### Semicolons
|
||||
|
||||
Always include semicolons after every property. When the stylesheets are
|
||||
minified, the semicolons will be removed automatically.
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
.container-item {
|
||||
width: 100px;
|
||||
height: 100px
|
||||
}
|
||||
|
||||
// Good
|
||||
.container-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
```
|
||||
|
||||
### Shorthand
|
||||
|
||||
The shorthand form should be used for properties that support it.
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
margin: 10px 15px 10px 15px;
|
||||
padding: 10px 10px 10px 10px;
|
||||
|
||||
// Good
|
||||
margin: 10px 15px;
|
||||
padding: 10px;
|
||||
```
|
||||
|
||||
### Zero Units
|
||||
|
||||
Omit length units on zero values, they're unnecessary and not including them
|
||||
is slightly more performant.
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
.item-with-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
// Good
|
||||
.item-with-padding {
|
||||
padding: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Selectors with a `js-` Prefix
|
||||
Do not use any selector prefixed with `js-` for styling purposes. These
|
||||
selectors are intended for use only with JavaScript to allow for removal or
|
||||
renaming without breaking styling.
|
||||
|
||||
## Linting
|
||||
|
||||
We use [SCSS Lint][scss-lint] to check for style guide conformity. It uses the
|
||||
ruleset in `.scss-lint.yml`, which is located in the home directory of the
|
||||
project.
|
||||
|
||||
To check if any warnings will be produced by your changes, you can run `rake
|
||||
scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
|
||||
catch any warnings.
|
||||
|
||||
If the Rake task is throwing warnings you don't understand, SCSS Lint's
|
||||
documentation includes [a full list of their linters][scss-lint-documentation].
|
||||
|
||||
### Fixing issues
|
||||
|
||||
If you want to automate changing a large portion of the codebase to conform to
|
||||
the SCSS style guide, you can use [CSSComb][csscomb]. First install
|
||||
[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
|
||||
CSSComb globally (system-wide). Run it in the GitLab directory with
|
||||
`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
|
||||
|
||||
Note that this won't fix every problem, but it should fix a majority.
|
||||
|
||||
[csscomb]: https://github.com/csscomb/csscomb.js
|
||||
[node]: https://github.com/nodejs/node
|
||||
[npm]: https://www.npmjs.com/
|
||||
[scss-lint]: https://github.com/brigade/scss-lint
|
||||
[scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
|
|
@ -76,8 +76,7 @@ Feature: Profile
|
|||
|
||||
Scenario: I can manage application
|
||||
Given I visit profile applications page
|
||||
Then I click on new application button
|
||||
And I should see application form
|
||||
Then I should see application form
|
||||
Then I fill application form out and submit
|
||||
And I see application
|
||||
Then I click edit
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
@labels
|
||||
Feature: Labels
|
||||
Background:
|
||||
Given I sign in as a user
|
||||
And I own project "Shop"
|
||||
And project "Shop" has labels: "bug", "feature", "enhancement"
|
||||
When I visit project "Shop" labels page
|
||||
|
||||
@javascript
|
||||
Scenario: I can subscribe to a label
|
||||
Then I should see that I am not subscribed to the "bug" label
|
||||
When I click button "Subscribe" for the "bug" label
|
||||
Then I should see that I am subscribed to the "bug" label
|
||||
When I click button "Unsubscribe" for the "bug" label
|
||||
Then I should see that I am not subscribed to the "bug" label
|
|
@ -46,11 +46,18 @@ Feature: Project Merge Requests
|
|||
Then I should see "Feature NS-03" in merge requests
|
||||
And I should see "Bug NS-04" in merge requests
|
||||
|
||||
Scenario: I visit merge request page
|
||||
Scenario: I visit an open merge request page
|
||||
Given I click link "Bug NS-04"
|
||||
Then I should see merge request "Bug NS-04"
|
||||
And I should see "1 of 1" in the sidebar
|
||||
|
||||
Scenario: I visit a merged merge request page
|
||||
Given project "Shop" have "Feature NS-05" merged merge request
|
||||
And I click link "Merged"
|
||||
And I click link "Feature NS-05"
|
||||
Then I should see merge request "Feature NS-05"
|
||||
And I should see "3 of 3" in the sidebar
|
||||
|
||||
Scenario: I close merge request page
|
||||
Given I click link "Bug NS-04"
|
||||
And I click link "Close"
|
||||
|
|
|
@ -27,7 +27,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I change my avatar' do
|
||||
attach_avatar
|
||||
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
|
||||
click_button "Update profile settings"
|
||||
@user.reload
|
||||
end
|
||||
|
||||
step 'I should see new avatar' do
|
||||
|
@ -40,7 +42,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I have an avatar' do
|
||||
attach_avatar
|
||||
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
|
||||
click_button "Update profile settings"
|
||||
@user.reload
|
||||
end
|
||||
|
||||
step 'I remove my avatar' do
|
||||
|
@ -180,18 +184,14 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
end
|
||||
end
|
||||
|
||||
step 'I click on new application button' do
|
||||
click_on 'New Application'
|
||||
end
|
||||
|
||||
step 'I should see application form' do
|
||||
expect(page).to have_content "New Application"
|
||||
expect(page).to have_content "Add new application"
|
||||
end
|
||||
|
||||
step 'I fill application form out and submit' do
|
||||
fill_in :doorkeeper_application_name, with: 'test'
|
||||
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
|
||||
click_on "Submit"
|
||||
click_on "Save application"
|
||||
end
|
||||
|
||||
step 'I see application' do
|
||||
|
@ -211,7 +211,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
step 'I change name of application and submit' do
|
||||
expect(page).to have_content "Edit application"
|
||||
fill_in :doorkeeper_application_name, with: 'test_changed'
|
||||
click_on "Submit"
|
||||
click_on "Save application"
|
||||
end
|
||||
|
||||
step 'I see that application was changed' do
|
||||
|
@ -229,16 +229,4 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
step "I see that application is removed" do
|
||||
expect(page.find(".oauth-applications")).not_to have_content "test_changed"
|
||||
end
|
||||
|
||||
def attach_avatar
|
||||
attach_file :user_avatar, Rails.root.join(*%w(spec fixtures banana_sample.gif))
|
||||
|
||||
page.find('#user_avatar_crop_x', visible: false).set('0')
|
||||
page.find('#user_avatar_crop_y', visible: false).set('0')
|
||||
page.find('#user_avatar_crop_size', visible: false).set('256')
|
||||
|
||||
click_button "Update profile settings"
|
||||
|
||||
@user.reload
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,6 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
|
|||
thumbsup = page.first('.award-control')
|
||||
thumbsup.click
|
||||
thumbsup.hover
|
||||
sleep 0.3
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -46,12 +45,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I have award added' do
|
||||
sleep 0.2
|
||||
|
||||
page.within '.awards' do
|
||||
expect(page).to have_selector '.js-emoji-btn'
|
||||
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
|
||||
expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me')
|
||||
expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
class Spinach::Features::Labels < Spinach::FeatureSteps
|
||||
include SharedAuthentication
|
||||
include SharedIssuable
|
||||
include SharedProject
|
||||
include SharedNote
|
||||
include SharedPaths
|
||||
include SharedMarkdown
|
||||
|
||||
step 'And I visit project "Shop" labels page' do
|
||||
visit namespace_project_labels_path(project.namespace, project)
|
||||
end
|
||||
|
||||
step 'I should see that I am subscribed to the "bug" label' do
|
||||
expect(subscribe_button).to have_content 'Unsubscribe'
|
||||
end
|
||||
|
||||
step 'I should see that I am not subscribed to the "bug" label' do
|
||||
expect(subscribe_button).to have_content 'Subscribe'
|
||||
end
|
||||
|
||||
step 'I click button "Unsubscribe" for the "bug" label' do
|
||||
subscribe_button.click
|
||||
end
|
||||
|
||||
step 'I click button "Subscribe" for the "bug" label' do
|
||||
subscribe_button.click
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subscribe_button
|
||||
first('.subscribe-button span')
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue