Commit cce36cb3 authored by Rémy Coutable's avatar Rémy Coutable

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

parents 373c8008 ac8c6f24
{
"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
}
...@@ -125,6 +125,14 @@ rubocop: ...@@ -125,6 +125,14 @@ rubocop:
- ruby - ruby
- mysql - mysql
scss-lint:
stage: test
script:
- bundle exec rake scss_lint
tags:
- ruby
allow_failure: true
brakeman: brakeman:
stage: test stage: test
script: script:
...@@ -151,6 +159,8 @@ flay: ...@@ -151,6 +159,8 @@ flay:
bundler:audit: bundler:audit:
stage: test stage: test
only:
- master
script: script:
- "bundle exec bundle-audit update" - "bundle exec bundle-audit update"
- "bundle exec bundle-audit check --ignore OSVDB-115941" - "bundle exec bundle-audit check --ignore OSVDB-115941"
......
# 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
...@@ -10,7 +10,6 @@ v 8.6.0 (unreleased) ...@@ -10,7 +10,6 @@ v 8.6.0 (unreleased)
setup. A password can be provided during setup (see installation docs), or 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. GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki - 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 - 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 - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
...@@ -25,11 +24,13 @@ v 8.6.0 (unreleased) ...@@ -25,11 +24,13 @@ v 8.6.0 (unreleased)
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml` - Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance - Refactor and greatly improve search performance
- Add support for cross-project label references - 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 - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - Allow search for logged out users
- Allow to define on which builds the current one depends on - Allow to define on which builds the current one depends on
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view - 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) - Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino) - Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views - Show labels in dashboard and group milestone views
...@@ -39,6 +40,9 @@ v 8.6.0 (unreleased) ...@@ -39,6 +40,9 @@ v 8.6.0 (unreleased)
- Move group activity to separate page - Move group activity to separate page
- Continue parameters are checked to ensure redirection goes to the same instance - 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 v 8.5.5
- Ensure removing a project removes associated Todo entries - Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed - Prevent a 500 error in Todos when author was removed
......
...@@ -427,6 +427,7 @@ merge request: ...@@ -427,6 +427,7 @@ merge request:
1. [Rails](https://github.com/bbatsov/rails-style-guide) 1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) 1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab 1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md) 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 ...@@ -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-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 [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [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 [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 [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/ [`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
......
...@@ -82,9 +82,6 @@ gem "haml-rails", '~> 0.9.0' ...@@ -82,9 +82,6 @@ gem "haml-rails", '~> 0.9.0'
# Files attachments # Files attachments
gem "carrierwave", '~> 0.10.0' gem "carrierwave", '~> 0.10.0'
# Image editing
gem "mini_magick", '~> 4.4.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
...@@ -296,6 +293,7 @@ group :development, :test do ...@@ -296,6 +293,7 @@ group :development, :test do
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.35.0', require: false gem 'rubocop', '~> 0.35.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false gem 'simplecov', '~> 0.10.0', require: false
gem 'flog', require: false gem 'flog', require: false
......
...@@ -493,7 +493,6 @@ GEM ...@@ -493,7 +493,6 @@ GEM
method_source (0.8.2) method_source (0.8.2)
mime-types (1.25.1) mime-types (1.25.1)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_magick (4.4.0)
mini_portile2 (2.0.0) mini_portile2 (2.0.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
...@@ -742,6 +741,9 @@ GEM ...@@ -742,6 +741,9 @@ GEM
sawyer (0.6.0) sawyer (0.6.0)
addressable (~> 2.3.5) addressable (~> 2.3.5)
faraday (~> 0.8, < 0.10) faraday (~> 0.8, < 0.10)
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
sdoc (0.3.20) sdoc (0.3.20)
json (>= 1.1.3) json (>= 1.1.3)
rdoc (~> 3.10) rdoc (~> 3.10)
...@@ -989,7 +991,6 @@ DEPENDENCIES ...@@ -989,7 +991,6 @@ DEPENDENCIES
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.6.1) mail_room (~> 0.6.1)
method_source (~> 0.8) method_source (~> 0.8)
mini_magick (~> 4.4.0)
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16) mysql2 (~> 0.3.16)
...@@ -1042,6 +1043,7 @@ DEPENDENCIES ...@@ -1042,6 +1043,7 @@ DEPENDENCIES
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.0) sass-rails (~> 5.0.0)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20) sdoc (~> 0.3.20)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
......
...@@ -43,7 +43,6 @@ ...@@ -43,7 +43,6 @@
#= require jquery.nicescroll #= require jquery.nicescroll
#= require_tree . #= require_tree .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper.js
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......
...@@ -10,7 +10,6 @@ class @Breakpoints ...@@ -10,7 +10,6 @@ class @Breakpoints
setup: -> setup: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) -> allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}" ".device-#{breakpoint}"
return if $(allDeviceSelector.join(",")).length return if $(allDeviceSelector.join(",")).length
# Create all the elements # Create all the elements
...@@ -18,12 +17,17 @@ class @Breakpoints ...@@ -18,12 +17,17 @@ class @Breakpoints
"<div class='device-#{breakpoint} visible-#{breakpoint}'></div>" "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
$("body").append els.join('') $("body").append els.join('')
getBreakpointSize: -> visibleDevice: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) -> allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}" ".device-#{breakpoint}"
$(allDeviceSelector.join(",")).filter(":visible")
$visibleDevice = $(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] return $visibleDevice.attr("class").split("visible-")[1]
@get: -> @get: ->
......
...@@ -17,52 +17,14 @@ class @Profile ...@@ -17,52 +17,14 @@ class @Profile
$('.update-notifications').on 'ajax:complete', -> $('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
# Avatar management $('.js-choose-user-avatar-button').bind "click", ->
form = $(this).closest("form")
$avatarInput = $('.js-user-avatar-input') form.find(".js-user-avatar-input").click()
$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") form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '') filename = $(this).val().replace(/^.*[\\\/]/, '')
$filename.data('label', $filename.text()).text(filename) form.find(".js-avatar-filename").text(filename)
reader = new FileReader
reader.onload = (event) ->
$modalCrop.modal('show')
$modalCropImg.attr('src', event.target.result)
fileData = reader.readAsDataURL(this.files[0])
$ -> $ ->
# Extract the SSH Key title from its comment # Extract the SSH Key title from its comment
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
*= require_self *= require_self
*= require dropzone/basic *= require dropzone/basic
*= require cal-heatmap *= require cal-heatmap
*= require cropper.css
*/ */
/* /*
......
...@@ -120,6 +120,10 @@ ...@@ -120,6 +120,10 @@
.cover-desc { .cover-desc {
padding: 0 $gl-padding 3px; padding: 0 $gl-padding 3px;
color: $gl-text-color; color: $gl-text-color;
&.username:last-child {
padding-bottom: $gl-padding;
}
} }
.cover-controls { .cover-controls {
......
...@@ -41,12 +41,6 @@ ...@@ -41,12 +41,6 @@
transition: $transition; transition: $transition;
} }
@mixin transform($transform) {
-webkit-transform: $transform;
-ms-transform: $transform;
transform: $transform;
}
/** /**
* Prefilled mixins * Prefilled mixins
* Mixins with fixed values * Mixins with fixed values
......
...@@ -103,6 +103,10 @@ $border-red-dark: #CA264F; ...@@ -103,6 +103,10 @@ $border-red-dark: #CA264F;
$help-well-bg: #FAFAFA; $help-well-bg: #FAFAFA;
$help-well-border: #E5E5E5; $help-well-border: #E5E5E5;
$warning-message-bg: #FBF2D9;
$warning-message-color: #9E8E60;
$warning-message-border: #F0E2BB;
/* header */ /* header */
$light-grey-header: #faf9f9; $light-grey-header: #faf9f9;
......
...@@ -109,42 +109,6 @@ ...@@ -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 {
.key-list-item-info { .key-list-item-info {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
...@@ -215,3 +179,21 @@ ...@@ -215,3 +179,21 @@
color: $provider-btn-not-active-color; 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;
}
}
...@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
layout 'profile' layout 'profile'
def index def index
head :forbidden and return set_index_vars
end end
def create def create
...@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application) redirect_to oauth_application_url(@application)
else else
render :new set_index_vars
render :index
end end
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 private
def verify_user_oauth_applications_enabled def verify_user_oauth_applications_enabled
...@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
redirect_to applications_profile_url redirect_to applications_profile_url
end 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 def set_application
@application = current_user.oauth_applications.find(params[:id]) @application = current_user.oauth_applications.find(params[:id])
end end
......
...@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController
def show def show
end 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 def update
user_params.except!(:email) if @user.ldap_user? user_params.except!(:email) if @user.ldap_user?
...@@ -65,9 +58,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -65,9 +58,6 @@ class ProfilesController < Profiles::ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:avatar_crop_x,
:avatar_crop_y,
:avatar_crop_size,
:avatar, :avatar,
:bio, :bio,
:email, :email,
......
...@@ -173,10 +173,15 @@ class ProjectsController < ApplicationController ...@@ -173,10 +173,15 @@ class ProjectsController < ApplicationController
def housekeeping def housekeeping
::Projects::HousekeepingService.new(@project).execute ::Projects::HousekeepingService.new(@project).execute
respond_to do |format| redirect_to(
flash[:notice] = "Housekeeping successfully started." project_path(@project),
format.html { redirect_to project_path(@project) } notice: "Housekeeping successfully started"
end )
rescue ::Projects::HousekeepingService::LeaseTaken => ex
redirect_to(
edit_project_path(@project),
alert: ex.to_s
)
end end
def toggle_star def toggle_star
......
...@@ -12,9 +12,13 @@ module CiStatusHelper ...@@ -12,9 +12,13 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status) ci_label_for_status(ci_commit.status)
end end
def ci_status_with_icon(status) def ci_status_with_icon(status, target = nil)
content_tag :span, class: "ci-status ci-#{status}" do content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
ci_icon_for_status(status) + '&nbsp;'.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
end end
......
...@@ -3,7 +3,7 @@ module EventsHelper ...@@ -3,7 +3,7 @@ module EventsHelper
author = event.author author = event.author
if author if author
link_to author.name, user_path(author.username) link_to author.name, user_path(author.username), title: h(author.name)
else else
event.author_name event.author_name
end end
...@@ -159,7 +159,7 @@ module EventsHelper ...@@ -159,7 +159,7 @@ module EventsHelper
link_to( link_to(
namespace_project_commit_path(event.project.namespace, event.project, namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id, event.note_commit_id,
anchor: dom_id(event.target)), anchor: dom_id(event.target), title: h(event.target_title)),
class: "commit_short_id" class: "commit_short_id"
) do ) do
"#{event.note_target_type} #{event.note_short_commit_id}" "#{event.note_target_type} #{event.note_short_commit_id}"
...@@ -167,7 +167,7 @@ module EventsHelper ...@@ -167,7 +167,7 @@ module EventsHelper
elsif event.note_project_snippet? elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace, link_to(namespace_project_snippet_path(event.project.namespace,
event.project, 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}" "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end end
else else
......
...@@ -31,7 +31,11 @@ module IssuablesHelper ...@@ -31,7 +31,11 @@ module IssuablesHelper
end end
def issuable_state_scope(issuable) def issuable_state_scope(issuable)
if issuable.respond_to?(:merged?) && issuable.merged?
:merged
else
issuable.open? ? :opened : :closed issuable.open? ? :opened : :closed
end end
end
end end
...@@ -8,7 +8,7 @@ module ProjectsHelper ...@@ -8,7 +8,7 @@ module ProjectsHelper
end end
def link_to_project(project) 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') title = content_tag(:span, project.name, class: 'project-name')
if project.namespace if project.namespace
......
...@@ -16,7 +16,7 @@ module TodosHelper ...@@ -16,7 +16,7 @@ module TodosHelper
def todo_target_link(todo) def todo_target_link(todo)
target = todo.target_type.titleize.downcase 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 end
def todo_target_path(todo) def todo_target_path(todo)
......
...@@ -14,7 +14,10 @@ module Emails ...@@ -14,7 +14,10 @@ module Emails
end end
def new_ssh_key_email(key_id) 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 @current_user = @user = @key.user
@target_url = user_url(@user) @target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account")) mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
require 'digest/md5' require 'digest/md5'
class Key < ActiveRecord::Base class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable include Sortable
belongs_to :user belongs_to :user
...@@ -64,7 +65,7 @@ class Key < ActiveRecord::Base ...@@ -64,7 +65,7 @@ class Key < ActiveRecord::Base
end end
def notify_user def notify_user
NotificationService.new.new_key(self) run_after_commit { NotificationService.new.new_key(self) }
end end
def post_create_hook def post_create_hook
......
...@@ -320,7 +320,14 @@ class Project < ActiveRecord::Base ...@@ -320,7 +320,14 @@ class Project < ActiveRecord::Base
or(ptable[:description].matches(pattern)) 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). namespaces = select(:id).
except(:includes).
joins(:namespace). joins(:namespace).
where(ntable[:name].matches(pattern)) where(ntable[:name].matches(pattern))
...@@ -598,6 +605,7 @@ class Project < ActiveRecord::Base ...@@ -598,6 +605,7 @@ class Project < ActiveRecord::Base
end end
def external_issue_tracker def external_issue_tracker
return @external_issue_tracker if defined?(@external_issue_tracker)
@external_issue_tracker ||= @external_issue_tracker ||=
services.issue_trackers.active.without_defaults.first services.issue_trackers.active.without_defaults.first
end end
......
...@@ -98,9 +98,6 @@ class User < ActiveRecord::Base ...@@ -98,9 +98,6 @@ class User < ActiveRecord::Base
# Virtual attribute for authenticating by either username or email # Virtual attribute for authenticating by either username or email
attr_accessor :login attr_accessor :login
# Virtual attributes to define avatar cropping
attr_accessor :avatar_crop_x, :avatar_crop_y, :avatar_crop_size
# #
# Relations # Relations
# #
...@@ -168,11 +165,6 @@ class User < ActiveRecord::Base ...@@ -168,11 +165,6 @@ class User < ActiveRecord::Base
validate :owns_public_email, if: ->(user) { user.public_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } 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 :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create before_validation :restricted_signup_domains, on: :create
before_validation :sanitize_attrs before_validation :sanitize_attrs
......
...@@ -49,6 +49,8 @@ class GitPushService < BaseService ...@@ -49,6 +49,8 @@ class GitPushService < BaseService
# Update merge requests that may be affected by this push. A new branch # Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change. # could cause the last commit of a merge request to change.
update_merge_requests update_merge_requests
perform_housekeeping
end end
def update_main_language def update_main_language
...@@ -87,6 +89,13 @@ class GitPushService < BaseService ...@@ -87,6 +89,13 @@ class GitPushService < BaseService
) )
end 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 def process_default_branch
@push_commits = project.repository.commits(params[:newrev]) @push_commits = project.repository.commits(params[:newrev])
...@@ -94,7 +103,7 @@ class GitPushService < BaseService ...@@ -94,7 +103,7 @@ class GitPushService < BaseService
project.change_head(branch_name) project.change_head(branch_name)
# Set protection on the default branch if configured # 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 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 }) @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
end end
......
...@@ -9,12 +9,39 @@ module Projects ...@@ -9,12 +9,39 @@ module Projects
class HousekeepingService < BaseService class HousekeepingService < BaseService
include Gitlab::ShellAdapter 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) def initialize(project)
@project = project @project = project
end end
def execute def execute
raise LeaseTaken if !try_obtain_lease
GitlabShellWorker.perform_async(:gc, @project.path_with_namespace) 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 end
end end
...@@ -2,22 +2,11 @@ ...@@ -2,22 +2,11 @@
class AvatarUploader < CarrierWave::Uploader::Base class AvatarUploader < CarrierWave::Uploader::Base
include UploaderHelper include UploaderHelper
include CarrierWave::MiniMagick
storage :file storage :file
after :store, :reset_events_cache 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 def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end end
......
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm' - submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_application_path(application) do = form_tag oauth_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/ %input{:name => "_method", :type => "hidden", :value => "delete"}/
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css - if defined? small
\ No newline at end of file = 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
= 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? - if application.errors.any?
.alert.alert-danger .alert.alert-danger
%ul %ul
...@@ -6,15 +6,11 @@ ...@@ -6,15 +6,11 @@
%li= msg %li= msg
.form-group .form-group
= f.label :name, class: 'control-label' = f.label :name, class: 'label-light'
.col-sm-10
= f.text_field :name, class: 'form-control', required: true = f.text_field :name, class: 'form-control', required: true
.form-group .form-group
= f.label :redirect_uri, class: 'control-label' = f.label :redirect_uri, class: 'label-light'
.col-sm-10
= f.text_area :redirect_uri, class: 'form-control', required: true = f.text_area :redirect_uri, class: 'form-control', required: true
%span.help-block %span.help-block
...@@ -25,6 +21,5 @@ ...@@ -25,6 +21,5 @@
%code= Doorkeeper.configuration.native_redirect_uri %code= Doorkeeper.configuration.native_redirect_uri
for local tests for local tests
.form-actions .prepend-top-default
= f.submit 'Submit', class: "btn btn-create" = f.submit 'Save application', class: "btn btn-create"
= link_to "Cancel", applications_profile_path, class: "btn btn-cancel"
- page_title "Applications" - page_title "Applications"
%h3.page-title Your applications - header_title page_title, applications_profile_path
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
.table-holder .row.prepend-top-default
%table.table.table-striped .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 %thead
%tr %tr
%th Name %th Name
%th Callback URL %th Callback URL
%th %th Clients
%th %th.last-heading
%tbody %tbody
- @applications.each do |application| - @applications.each do |application|
%tr{:id => "application_#{application.id}"} %tr{id: "application_#{application.id}"}
%td= link_to application.name, oauth_application_path(application) %td= link_to application.name, oauth_application_path(application)
%td= application.redirect_uri %td
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link' - application.redirect_uri.split.each do |uri|
%td= render 'delete_form', application: application %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
%li.commit %li.commit
.commit-row-title .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])
&middot; &middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.event-last-push .event-last-push
.event-last-push-text .event-last-push-text
%span You pushed to %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 %strong= event.ref_name
%span at %span at
%strong= link_to_project event.project %strong= link_to_project event.project
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%strong= event.ref_name %strong= event.ref_name
- else - else
%strong %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 at
= link_to_project event.project = link_to_project event.project
......
.top-area = render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
.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
- if projects.present? = render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
.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
...@@ -27,24 +27,33 @@ ...@@ -27,24 +27,33 @@
.cover-desc.description .cover-desc.description
= markdown(@group.description, pipeline: :description) = markdown(@group.description, pipeline: :description)
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.top-area
%ul.nav-links %ul.nav-links
%li.active %li.active
= link_to "#projects", 'data-toggle' => 'tab' do = link_to "#projects", 'data-toggle' => 'tab' do
Projects All Projects
- if @shared_projects.present? - if @shared_projects.present?
%li %li
= link_to "#shared", 'data-toggle' => 'tab' do = link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects 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
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.tab-content .tab-content
.tab-pane.active#projects .tab-pane.active#projects
= render "projects", projects: @projects = render "projects", projects: @projects
.tab-pane#shared .tab-pane#shared
= render "shared_projects", projects: @shared_projects = render "shared_projects", projects: @shared_projects
- if @shared_projects.present?
.tab-pane#shared .tab-pane#shared
= render "shared_projects", projects: @shared_projects = render "shared_projects", projects: @shared_projects
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= icon('gear fw') = icon('gear fw')
%span %span
Account 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 = link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw') = icon('cloud fw')
%span %span
......
- 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
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
= f.hidden_field :avatar_crop_x
= f.hidden_field :avatar_crop_y
= f.hidden_field :avatar_crop_size
-if @user.errors.any? -if @user.errors.any?
%div.alert.alert-danger %div.alert.alert-danger
%ul %ul
...@@ -97,19 +94,3 @@ ...@@ -97,19 +94,3 @@
.prepend-top-default.append-bottom-default .prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: "btn btn-success" = f.submit 'Update profile settings', class: "btn btn-success"
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel" = 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
&times;
%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
%tr.build %tr.build
%td.status %td.status
- if can?(current_user, :read_build, build) - if can?(current_user, :read_build, build)
= link_to namespace_project_build_url(build.project.namespace, build.project, build), class: "ci-status ci-#{build.status}" do = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
= ci_status_with_icon(build.status)
- else - else
= ci_status_with_icon(build.status) = ci_status_with_icon(build.status)
......
%tr.generic_commit_status %tr.generic_commit_status
%td.status %td.status
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
= link_to generic_commit_status.target_url, class: "ci-status ci-#{generic_commit_status.status}" do = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
= ci_status_with_icon(generic_commit_status.status)
- else - else
= ci_status_with_icon(generic_commit_status.status) = ci_status_with_icon(generic_commit_status.status)
......
...@@ -312,7 +312,7 @@ Rails.application.routes.draw do ...@@ -312,7 +312,7 @@ Rails.application.routes.draw do
resource :profile, only: [:show, :update] do resource :profile, only: [:show, :update] do
member do member do
get :audit_log get :audit_log
get :applications get :applications, to: 'oauth/applications#index'
put :reset_private_token put :reset_private_token
put :update_username put :update_username
......
class ProjectsAddPushesSinceGc < ActiveRecord::Migration
def change
add_column :projects, :pushes_since_gc, :integer, default: 0
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160309140734) do ActiveRecord::Schema.define(version: 20160314143402) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -827,6 +827,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do ...@@ -827,6 +827,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do
t.boolean "pending_delete", default: false t.boolean "pending_delete", default: false
t.boolean "public_builds", default: true, null: false t.boolean "public_builds", default: true, null: false
t.string "main_language" t.string "main_language"
t.integer "pushes_since_gc", default: 0
end end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
......
## Test and Deploy a ruby application ## 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). 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).
......
...@@ -399,7 +399,7 @@ The above script will: ...@@ -399,7 +399,7 @@ The above script will:
>**Notes:** >**Notes:**
> >
> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms. > - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
> - Limited Windows support was added in GitLab Runner v.1.0.0. > - Windows support was added in GitLab Runner v.1.0.0.
> - Currently not all executors are supported. > - Currently not all executors are supported.
> - Build artifacts are only collected for successful builds. > - Build artifacts are only collected for successful builds.
......
# 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 ...@@ -76,8 +76,7 @@ Feature: Profile
Scenario: I can manage application Scenario: I can manage application
Given I visit profile applications page Given I visit profile applications page
Then I click on new application button Then I should see application form
And I should see application form
Then I fill application form out and submit Then I fill application form out and submit
And I see application And I see application
Then I click edit Then I click edit
......
...@@ -46,11 +46,18 @@ Feature: Project Merge Requests ...@@ -46,11 +46,18 @@ Feature: Project Merge Requests
Then I should see "Feature NS-03" in merge requests Then I should see "Feature NS-03" in merge requests
And I should see "Bug NS-04" 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" Given I click link "Bug NS-04"
Then I should see merge request "Bug NS-04" Then I should see merge request "Bug NS-04"
And I should see "1 of 1" in the sidebar 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 Scenario: I close merge request page
Given I click link "Bug NS-04" Given I click link "Bug NS-04"
And I click link "Close" And I click link "Close"
......
...@@ -27,7 +27,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -27,7 +27,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end end
step 'I change my avatar' do 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 end
step 'I should see new avatar' do step 'I should see new avatar' do
...@@ -40,7 +42,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -40,7 +42,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end end
step 'I have an avatar' do 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 end
step 'I remove my avatar' do step 'I remove my avatar' do
...@@ -180,18 +184,14 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -180,18 +184,14 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end end
end end
step 'I click on new application button' do
click_on 'New Application'
end
step 'I should see application form' do step 'I should see application form' do
expect(page).to have_content "New Application" expect(page).to have_content "Add new application"
end end
step 'I fill application form out and submit' do step 'I fill application form out and submit' do
fill_in :doorkeeper_application_name, with: 'test' fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
click_on "Submit" click_on "Save application"
end end
step 'I see application' do step 'I see application' do
...@@ -211,7 +211,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -211,7 +211,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I change name of application and submit' do step 'I change name of application and submit' do
expect(page).to have_content "Edit application" expect(page).to have_content "Edit application"
fill_in :doorkeeper_application_name, with: 'test_changed' fill_in :doorkeeper_application_name, with: 'test_changed'
click_on "Submit" click_on "Save application"
end end
step 'I see that application was changed' do step 'I see that application was changed' do
...@@ -229,16 +229,4 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -229,16 +229,4 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step "I see that application is removed" do step "I see that application is removed" do
expect(page.find(".oauth-applications")).not_to have_content "test_changed" expect(page.find(".oauth-applications")).not_to have_content "test_changed"
end 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 end
...@@ -16,10 +16,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -16,10 +16,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_link "Bug NS-04" click_link "Bug NS-04"
end end
step 'I click link "Feature NS-05"' do
click_link "Feature NS-05"
end
step 'I click link "All"' do step 'I click link "All"' do
click_link "All" click_link "All"
end end
step 'I click link "Merged"' do
click_link "Merged"
end
step 'I click link "Closed"' do step 'I click link "Closed"' do
click_link "Closed" click_link "Closed"
end end
...@@ -40,6 +48,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -40,6 +48,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content "Bug NS-04" expect(page).to have_content "Bug NS-04"
end end
step 'I should see merge request "Feature NS-05"' do
expect(page).to have_content "Feature NS-05"
end
step 'I should not see "master" branch' do step 'I should not see "master" branch' do
expect(find('.merge-request-info')).not_to have_content "master" expect(find('.merge-request-info')).not_to have_content "master"
end end
...@@ -120,6 +132,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -120,6 +132,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first) author: project.users.first)
end end
step 'project "Shop" have "Feature NS-05" merged merge request' do
create(:merged_merge_request,
title: "Feature NS-05",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
create(:merge_request, :rebased, create(:merge_request, :rebased,
title: "Bug NS-07", title: "Bug NS-07",
......
...@@ -147,6 +147,10 @@ module SharedIssuable ...@@ -147,6 +147,10 @@ module SharedIssuable
expect_sidebar_content('2 of 2') expect_sidebar_content('2 of 2')
end end
step 'I should see "3 of 3" in the sidebar' do
expect_sidebar_content('3 of 3')
end
step 'I click link "Next" in the sidebar' do step 'I click link "Next" in the sidebar' do
page.within '.issuable-sidebar' do page.within '.issuable-sidebar' do
click_link 'Next' click_link 'Next'
......
...@@ -7,7 +7,7 @@ module Banzai ...@@ -7,7 +7,7 @@ module Banzai
# #
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter class SanitizationFilter < HTML::Pipeline::SanitizationFilter
UNSAFE_PROTOCOLS = %w(javascript :javascript data vbscript).freeze UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist def whitelist
whitelist = super whitelist = super
...@@ -64,7 +64,12 @@ module Banzai ...@@ -64,7 +64,12 @@ module Banzai
return unless node.name == 'a' return unless node.name == 'a'
return unless node.has_attribute?('href') return unless node.has_attribute?('href')
if node['href'].start_with?(*UNSAFE_PROTOCOLS) begin
uri = Addressable::URI.parse(node['href'])
uri.scheme.strip! if uri.scheme
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError
node.remove_attribute('href') node.remove_attribute('href')
end end
end end
......
unless Rails.env.production?
require 'scss_lint/rake_task'
SCSSLint::RakeTask.new do |t|
t.config = '.scss-lint.yml'
# See https://github.com/brigade/scss-lint/issues/726
# Hack, otherwise linter won't respect scss_files option in config file.
t.files = []
end
end
require 'spec_helper' require 'spec_helper'
describe NamespacesController do describe NamespacesController do
let!(:user) { create(:user, :with_avatar) } let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
describe "GET show" do describe "GET show" do
context "when the namespace belongs to a user" do context "when the namespace belongs to a user" do
......
require 'spec_helper' require 'spec_helper'
describe Profiles::AvatarsController do describe Profiles::AvatarsController do
let(:user) { create(:user, :with_avatar) } let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) }
before do before do
sign_in(user) sign_in(user)
......
require 'spec_helper' require 'spec_helper'
describe UploadsController do describe UploadsController do
let!(:user) { create(:user, :with_avatar) } let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
describe "GET show" do describe "GET show" do
context "when viewing a user avatar" do context "when viewing a user avatar" do
......
...@@ -56,6 +56,10 @@ FactoryGirl.define do ...@@ -56,6 +56,10 @@ FactoryGirl.define do
target_branch "feature" target_branch "feature"
end end
trait :merged do
state :merged
end
trait :closed do trait :closed do
state :closed state :closed
end end
...@@ -90,6 +94,7 @@ FactoryGirl.define do ...@@ -90,6 +94,7 @@ FactoryGirl.define do
merge_user author merge_user author
end end
factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed] factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened] factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs] factory :merge_request_with_diffs, traits: [:with_diffs]
......
...@@ -23,13 +23,6 @@ FactoryGirl.define do ...@@ -23,13 +23,6 @@ FactoryGirl.define do
end end
end end
trait :with_avatar do
avatar { fixture_file_upload(Rails.root.join(*%w(spec fixtures dk.png)), 'image/png') }
avatar_crop_x 0
avatar_crop_y 0
avatar_crop_size 256
end
factory :omniauth_user do factory :omniauth_user do
transient do transient do
extern_uid '123456' extern_uid '123456'
......
...@@ -77,7 +77,7 @@ describe ApplicationHelper do ...@@ -77,7 +77,7 @@ describe ApplicationHelper do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
it 'should return an url for the avatar' do it 'should return an url for the avatar' do
user = create(:user, :with_avatar, avatar: File.open(avatar_file_path)) user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s). expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
...@@ -88,7 +88,7 @@ describe ApplicationHelper do ...@@ -88,7 +88,7 @@ describe ApplicationHelper do
# Must be stubbed after the stub above, and separately # Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url)) stub_config_setting(url: Settings.send(:build_gitlab_url))
user = create(:user, :with_avatar, avatar: File.open(avatar_file_path)) user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s). expect(helper.avatar_icon(user.email).to_s).
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
...@@ -102,7 +102,7 @@ describe ApplicationHelper do ...@@ -102,7 +102,7 @@ describe ApplicationHelper do
describe 'using a User' do describe 'using a User' do
it 'should return an URL for the avatar' do it 'should return an URL for the avatar' do
user = create(:user, :with_avatar, avatar: File.open(avatar_file_path)) user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user).to_s). expect(helper.avatar_icon(user).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
......
...@@ -149,10 +149,20 @@ describe Banzai::Filter::SanitizationFilter, lib: true do ...@@ -149,10 +149,20 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
output: '<a href="java"></a>' output: '<a href="java"></a>'
}, },
'protocol-based JS injection: invalid URL char' => {
input: '<img src=java\script:alert("XSS")>',
output: '<img>'
},
'protocol-based JS injection: spaces and entities' => { 'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>', input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>' output: '<a href="">foo</a>'
}, },
'protocol whitespace' => {
input: '<a href=" http://example.com/"></a>',
output: '<a href="http://example.com/"></a>'
}
} }
protocols.each do |name, data| protocols.each do |name, data|
...@@ -177,6 +187,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do ...@@ -177,6 +187,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(output.to_html).to eq '<a>XSS</a>' expect(output.to_html).to eq '<a>XSS</a>'
end end
it 'disallows invalid URIs' do
expect(Addressable::URI).to receive(:parse).with('foo://example.com').
and_raise(Addressable::URI::InvalidURIError)
input = '<a href="foo://example.com">Foo</a>'
output = filter(input)
expect(output.to_html).to eq '<a>Foo</a>'
end
it 'allows non-standard anchor schemes' do it 'allows non-standard anchor schemes' do
exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>} exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
act = filter(exp) act = filter(exp)
......
...@@ -77,6 +77,10 @@ describe Notify do ...@@ -77,6 +77,10 @@ describe Notify do
it 'includes a link to ssh keys page' do it 'includes a link to ssh keys page' do
is_expected.to have_body_text /#{profile_keys_path}/ is_expected.to have_body_text /#{profile_keys_path}/
end end
context 'with SSH key that does not exist' do
it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
end
end end
describe 'user added email' do describe 'user added email' do
......
...@@ -716,6 +716,12 @@ describe Project, models: true do ...@@ -716,6 +716,12 @@ describe Project, models: true do
it 'returns projects with a matching namespace name regardless of the casing' do it 'returns projects with a matching namespace name regardless of the casing' do
expect(described_class.search(project.namespace.name.upcase)).to eq([project]) expect(described_class.search(project.namespace.name.upcase)).to eq([project])
end end
it 'returns projects when eager loading namespaces' do
relation = described_class.all.includes(:namespace)
expect(relation.search(project.namespace.name)).to eq([project])
end
end end
describe '#rename_repo' do describe '#rename_repo' do
......
...@@ -174,32 +174,6 @@ describe User, models: true do ...@@ -174,32 +174,6 @@ describe User, models: true do
end end
end end
end end
describe 'avatar' do
it 'only validates when avatar is present and changed' do
user = build(:user, :with_avatar)
user.avatar_crop_x = nil
user.avatar_crop_y = nil
user.avatar_crop_size = nil
expect(user).not_to be_valid
expect(user.errors.keys).
to match_array %i(avatar_crop_x avatar_crop_y avatar_crop_size)
end
it 'does not validate when avatar has not changed' do
user = create(:user, :with_avatar)
expect { user.avatar_crop_x = nil }.not_to change(user, :valid?)
end
it 'does not validate when avatar is not present' do
user = create(:user)
expect { user.avatar_crop_y = nil }.not_to change(user, :valid?)
end
end
end end
describe "non_ldap" do describe "non_ldap" do
......
...@@ -417,6 +417,45 @@ describe GitPushService, services: true do ...@@ -417,6 +417,45 @@ describe GitPushService, services: true do
end end
end end
describe "housekeeping" do
let(:housekeeping) { Projects::HousekeepingService.new(project) }
before do
allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping)
end
it 'does not perform housekeeping when not needed' do
expect(housekeeping).not_to receive(:execute)
execute_service(project, user, @oldrev, @newrev, @ref)
end
context 'when housekeeping is needed' do
before do
allow(housekeeping).to receive(:needed?).and_return(true)
end
it 'performs housekeeping' do
expect(housekeeping).to receive(:execute)
execute_service(project, user, @oldrev, @newrev, @ref)
end
it 'does not raise an exception' do
allow(housekeeping).to receive(:try_obtain_lease).and_return(false)
execute_service(project, user, @oldrev, @newrev, @ref)
end
end
it 'increments the push counter' do
expect(housekeeping).to receive(:increment!)
execute_service(project, user, @oldrev, @newrev, @ref)
end
end
def execute_service(project, user, oldrev, newrev, ref) def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute service.execute
......
require 'spec_helper'
describe Projects::HousekeepingService do
subject { Projects::HousekeepingService.new(project) }
let(:project) { create :project }
describe 'execute' do
before do
project.pushes_since_gc = 3
project.save!
end
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
subject.execute
expect(project.pushes_since_gc).to eq(0)
end
it 'does not enqueue a job when no lease can be obtained' do
expect(subject).to receive(:try_obtain_lease).and_return(false)
expect(GitlabShellWorker).not_to receive(:perform_async)
expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
expect(project.pushes_since_gc).to eq(0)
end
end
describe 'needed?' do
it 'when the count is low enough' do
expect(subject.needed?).to eq(false)
end
it 'when the count is high enough' do
allow(project).to receive(:pushes_since_gc).and_return(10)
expect(subject.needed?).to eq(true)
end
end
describe 'increment!' do
it 'increments the pushes_since_gc counter' do
expect(project.pushes_since_gc).to eq(0)
subject.increment!
expect(project.pushes_since_gc).to eq(1)
end
end
end
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment