Commit dcbbaffe authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee' into 'master'

CE master to EE

See merge request !305
parents 30e5b86b da3b158a
{ {
"exclude": [
"app/assets/stylesheets/framework/tw_bootstrap_variables.scss",
"app/assets/stylesheets/framework/fonts.scss"
],
"always-semicolon": true, "always-semicolon": true,
"color-case": "lower", "color-case": "lower",
"block-indent": " ", "block-indent": " ",
......
...@@ -131,7 +131,6 @@ scss-lint: ...@@ -131,7 +131,6 @@ scss-lint:
- bundle exec rake scss_lint - bundle exec rake scss_lint
tags: tags:
- ruby - ruby
allow_failure: true
brakeman: brakeman:
stage: test stage: test
......
...@@ -100,7 +100,7 @@ linters: ...@@ -100,7 +100,7 @@ linters:
# Selectors should always use hyphenated-lowercase, rather than camelCase or # Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case. # snake_case.
SelectorFormat: SelectorFormat:
enabled: true enabled: false
convention: hyphenated_lowercase convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it. # Prefer the shortest shorthand form possible for properties that support it.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
- Fix avatar stretching by providing a cropping feature
- Add links to CI setup documentation from project settings and builds pages
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
v 8.6.1 (unreleased) v 8.6.2 (unreleased)
- Comments on confidential issues don't show up in activity feed to non-members
v 8.6.1
- Add option to reload the schema before restoring a database backup. !2807
- Display navigation controls on mobile. !3214
- Fixed bug where participants would not work correctly on merge requests. !3329
- Fix sorting issues by votes on the groups issues page results in SQL errors. !3333
- Restrict notifications for confidential issues. !3334
- Do not allow to move issue if it has not been persisted. !3340
- Add a confirmation step before deleting an issuable. !3341
- Fixes issue with signin button overflowing on mobile. !3342
- Auto collapses the navigation sidebar when resizing. !3343
- Fix build dependencies, when the dependency is a string. !3344
- Shows error messages when trying to create label in dropdown menu. !3345
- Fixes issue with assign milestone not loading milestone list. !3346
- Fix an issue causing the Dashboard/Milestones page to be blank. !3348
v 8.6.0 v 8.6.0
- Add ability to move issue to another project - Add ability to move issue to another project
...@@ -31,7 +52,6 @@ v 8.6.0 ...@@ -31,7 +52,6 @@ v 8.6.0
- Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
- HTTP error pages work independently from location and config (Artem Sidorenko) - HTTP error pages work independently from location and config (Artem Sidorenko)
- 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
- Add option to reload the schema before restoring a database backup. !2807
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Added omniauth-auth0 Gem (Daniel Carraro) - Added omniauth-auth0 Gem (Daniel Carraro)
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
- [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight) - [Issue weight](#issue-weight)
- [Regression issues](#regression-issues) - [Regression issues](#regression-issues)
- [Technical debt](#technical-debt)
- [Merge requests](#merge-requests) - [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines) - [Merge request guidelines](#merge-request-guidelines)
- [Merge request description format](#merge-request-description-format) - [Merge request description format](#merge-request-description-format)
...@@ -242,6 +243,28 @@ addressed. ...@@ -242,6 +243,28 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127 [8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue [update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
### Technical debt
In order to track things that can be improved in GitLab's codebase, we created
the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
This label should be added to issues that describe things that can be improved,
shortcuts that have been taken, code that needs refactoring, features that need
additional attention, and all other things that have been left behind due to
high velocity of development.
Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels
can be combined with the `technical debt` label, to make it easier to schedule
the improvements for a release.
Issues tagged with the `technical debt` label have the same priority like issues
that describe a new feature to be introduced in GitLab, and should be scheduled
for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue.
## Merge requests ## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests, We welcome merge requests with fixes and improvements to GitLab code, tests,
......
...@@ -244,7 +244,7 @@ end ...@@ -244,7 +244,7 @@ end
group :development do group :development do
gem "foreman" gem "foreman"
gem 'brakeman', '~> 3.1.0', require: false gem 'brakeman', '~> 3.2.0', require: false
gem "annotate", "~> 2.6.0" gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2' gem "letter_opener", '~> 1.1.2'
...@@ -289,7 +289,7 @@ group :development, :test do ...@@ -289,7 +289,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0' gem 'teaspoon', '~> 1.1.0'
gem 'teaspoon-jasmine', '~> 2.2.0' gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.6.4' gem 'spring', '~> 1.6.4'
......
...@@ -84,21 +84,19 @@ GEM ...@@ -84,21 +84,19 @@ GEM
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.1.4) brakeman (3.2.1)
erubis (~> 2.6) erubis (~> 2.6)
fastercsv (~> 1.5)
haml (>= 3.0, < 5.0) haml (>= 3.0, < 5.0)
highline (>= 1.6.20, < 2.0) highline (>= 1.6.20, < 2.0)
multi_json (~> 1.2) ruby2ruby (~> 2.3.0)
ruby2ruby (>= 2.1.1, < 2.3.0) ruby_parser (~> 3.8.1)
ruby_parser (~> 3.7.0)
safe_yaml (>= 1.0) safe_yaml (>= 1.0)
sass (~> 3.0) sass (~> 3.0)
slim (>= 1.3.6, < 4.0) slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4) terminal-table (~> 1.4)
browser (1.0.1) browser (1.0.1)
builder (3.2.2) builder (3.2.2)
bullet (4.14.10) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0) uniform_notifier (~> 1.9.0)
bundler-audit (0.4.0) bundler-audit (0.4.0)
...@@ -221,7 +219,6 @@ GEM ...@@ -221,7 +219,6 @@ GEM
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
multi_json multi_json
fastercsv (1.5.5)
ffaker (2.0.0) ffaker (2.0.0)
ffi (1.9.10) ffi (1.9.10)
fission (0.5.0) fission (0.5.0)
...@@ -341,8 +338,8 @@ GEM ...@@ -341,8 +338,8 @@ GEM
fog-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.5.0.0) font-awesome-rails (4.5.0.1)
railties (>= 3.2, < 5.0) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
thor (~> 0.19.1) thor (~> 0.19.1)
formatador (0.2.5) formatador (0.2.5)
...@@ -730,10 +727,10 @@ GEM ...@@ -730,10 +727,10 @@ GEM
ruby-saml (1.1.2) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.2.0) ruby2ruby (2.3.0)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.0) sexp_processor (~> 4.0)
ruby_parser (3.7.2) ruby_parser (3.8.1)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
...@@ -742,7 +739,7 @@ GEM ...@@ -742,7 +739,7 @@ GEM
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.20) sass (3.4.21)
sass-rails (5.0.4) sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
sass (~> 3.1) sass (~> 3.1)
...@@ -766,7 +763,7 @@ GEM ...@@ -766,7 +763,7 @@ GEM
sentry-raven (0.15.6) sentry-raven (0.15.6)
faraday (>= 0.7.6) faraday (>= 0.7.6)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.6.0) sexp_processor (4.7.0)
sham_rack (1.3.6) sham_rack (1.3.6)
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
...@@ -830,8 +827,8 @@ GEM ...@@ -830,8 +827,8 @@ GEM
systemu (2.6.5) systemu (2.6.5)
task_list (1.0.2) task_list (1.0.2)
html-pipeline html-pipeline
teaspoon (1.0.2) teaspoon (1.1.5)
railties (>= 3.2.5, < 5) railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0) teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0) teaspoon (>= 1.0.0)
temple (0.7.6) temple (0.7.6)
...@@ -895,7 +892,7 @@ GEM ...@@ -895,7 +892,7 @@ GEM
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
warden (1.2.4) warden (1.2.4)
rack (>= 1.0) rack (>= 1.0)
web-console (2.2.1) web-console (2.3.0)
activemodel (>= 4.0) activemodel (>= 4.0)
binding_of_caller (>= 0.7.2) binding_of_caller (>= 0.7.2)
railties (>= 4.0) railties (>= 4.0)
...@@ -937,7 +934,7 @@ DEPENDENCIES ...@@ -937,7 +934,7 @@ DEPENDENCIES
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.1.0) brakeman (~> 3.2.0)
browser (~> 1.0.0) browser (~> 1.0.0)
bullet bullet
bundler-audit bundler-audit
...@@ -1081,7 +1078,7 @@ DEPENDENCIES ...@@ -1081,7 +1078,7 @@ DEPENDENCIES
sprockets (~> 3.3.5) sprockets (~> 3.3.5)
state_machines-activerecord (~> 0.3.0) state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2) task_list (~> 1.0.2)
teaspoon (~> 1.0.0) teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0) teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.6.1) thin (~> 1.6.1)
...@@ -1098,3 +1095,6 @@ DEPENDENCIES ...@@ -1098,3 +1095,6 @@ DEPENDENCIES
web-console (~> 2.0) web-console (~> 2.0)
webmock (~> 1.21.0) webmock (~> 1.21.0)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH
1.11.2
8.6.0-pre-ee 8.7.0-pre-ee
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
#= require jquery.nicescroll #= require jquery.nicescroll
#= require_tree . #= require_tree .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
...@@ -222,13 +223,20 @@ $ -> ...@@ -222,13 +223,20 @@ $ ->
$this = $(this) $this = $(this)
$this.attr 'value', $this.val() $this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle')
$navIconToggle = $('.toggle-nav-collapse')
$(document) $(document)
.off 'breakpoint:change' .off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) -> .on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs' if breakpoint is 'sm' or breakpoint is 'xs'
$gutterIcon = $('.js-sidebar-toggle').find('i') $gutterIcon = $sidebarGutterToggle.find('i')
if $gutterIcon.hasClass('fa-angle-double-right') if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click') $sidebarGutterToggle.trigger('click')
$navIcon = $navIconToggle.find('.fa')
if $navIcon.hasClass('fa-angle-left')
$navIconToggle.trigger('click')
$(document) $(document)
.off 'click', '.js-sidebar-toggle' .off 'click', '.js-sidebar-toggle'
......
class GitLabCrop
# Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/
constructor: (input, opts = {}) ->
@fileInput = $(input)
# We should rename to avoid spec to fail
# Form will submit the proper input filed with a file using FormData
@fileInput
.attr('name', "#{@fileInput.attr('name')}-trigger")
.attr('id', "#{@fileInput.attr('id')}-trigger")
# Set defaults
{
@exportWidth = 200
@exportHeight = 200
@cropBoxWidth = 200
@cropBoxHeight = 200
@form = @fileInput.parents('form')
# Required params
@filename
@previewImage
@modalCrop
@pickImageEl
@uploadImageBtn
@modalCropImg
} = opts
# Ensure needed elements are jquery objects
# If selector is provided we will convert them to a jQuery Object
@filename = @getElement(@filename)
@previewImage = @getElement(@previewImage)
@pickImageEl = @getElement(@pickImageEl)
# Modal elements usually are outside the @form element
@modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
@uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
@modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
@cropActionsBtn = @modalCrop.find('[data-method]')
@bindEvents()
getElement: (selector) ->
$(selector, @form)
bindEvents: ->
_this = @
@fileInput.on 'change', (e) ->
_this.onFileInputChange(e, @)
@pickImageEl.on 'click', @onPickImageClick
@modalCrop.on 'shown.bs.modal', @onModalShow
@modalCrop.on 'hidden.bs.modal', @onModalHide
@uploadImageBtn.on 'click', @onUploadImageBtnClick
@cropActionsBtn.on 'click', (e) ->
btn = @
_this.onActionBtnClick(btn)
@croppedImageBlob = null
onPickImageClick: =>
@fileInput.trigger('click')
onModalShow: =>
_this = @
@modalCropImg.cropper(
viewMode: 1
center: false
aspectRatio: 1
modal: true
scalable: false
rotatable: false
zoomable: true
dragMode: 'move'
guides: false
zoomOnTouch: false
zoomOnWheel: false
cropBoxMovable: false
cropBoxResizable: false
toggleDragModeOnDblclick: false
built: ->
$image = $(@)
container = $image.cropper 'getContainerData'
cropBoxWidth = _this.cropBoxWidth;
cropBoxHeight = _this.cropBoxHeight;
$image.cropper('setCropBoxData',
width: cropBoxWidth,
height: cropBoxHeight,
left: (container.width - cropBoxWidth) / 2,
top: (container.height - cropBoxHeight) / 2
)
)
onModalHide: =>
@modalCropImg
.attr('src', '') # Remove attached image
.cropper('destroy') # Destroy cropper instance
onUploadImageBtnClick: (e) =>
e.preventDefault()
@setBlob()
@setPreview()
@modalCrop.modal('hide')
@fileInput.val('')
onActionBtnClick: (btn) ->
data = $(btn).data()
if @modalCropImg.data('cropper') && data.method
result = @modalCropImg.cropper data.method, data.option
onFileInputChange: (e, input) ->
@readFile(input)
readFile: (input) ->
_this = @
reader = new FileReader
reader.onload = ->
_this.modalCropImg.attr('src', reader.result)
_this.modalCrop.modal('show')
reader.readAsDataURL(input.files[0])
dataURLtoBlob: (dataURL) ->
binary = atob(dataURL.split(',')[1])
array = []
for v, k in binary
array.push(binary.charCodeAt(k))
new Blob([new Uint8Array(array)], type: 'image/png')
setPreview: ->
@previewImage.attr('src', @dataURL)
filename = @fileInput.val().replace(FILENAMEREGEX, '')
@filename.text(filename)
setBlob: ->
@dataURL = @modalCropImg.cropper('getCroppedCanvas',
width: 200
height: 200
).toDataURL('image/png')
@croppedImageBlob = @dataURLtoBlob(@dataURL)
getBlob: ->
@croppedImageBlob
$.fn.glCrop = (opts) ->
return @.each ->
$(@).data('glcrop', new GitLabCrop(@, opts))
class GitLabDropdownFilter class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40] BLUR_KEYCODES = [27, 40]
HAS_VALUE_CLASS = "has-value"
constructor: (@dropdown, @options) -> constructor: (@input, @options) ->
@input = @dropdown.find(".dropdown-input .dropdown-input-field") $inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
# Clear click
$clearButton.on 'click', (e) =>
e.preventDefault()
e.stopPropagation()
@input
.val('')
.trigger('keyup')
.focus()
# Key events # Key events
timeout = "" timeout = ""
@input.on "keyup", (e) => @input.on "keyup", (e) =>
if e.keyCode is 13 && @input.val() isnt "" if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.addClass HAS_VALUE_CLASS
else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.removeClass HAS_VALUE_CLASS
if e.keyCode is 13 and @input.val() isnt ""
if @options.enterCallback if @options.enterCallback
@options.enterCallback() @options.enterCallback()
return return
...@@ -95,7 +111,9 @@ class GitLabDropdown ...@@ -95,7 +111,9 @@ class GitLabDropdown
# Init filiterable # Init filiterable
if @options.filterable if @options.filterable
@filter = new GitLabDropdownFilter @dropdown, @input = @dropdown.find('.dropdown-input .dropdown-input-field')
@filter = new GitLabDropdownFilter @input,
remote: @options.filterRemote remote: @options.filterRemote
query: @options.data query: @options.data
keys: @options.search.fields keys: @options.search.fields
...@@ -103,6 +121,7 @@ class GitLabDropdown ...@@ -103,6 +121,7 @@ class GitLabDropdown
return @fullData return @fullData
callback: (data) => callback: (data) =>
@parseData data @parseData data
@highlightRow 1
enterCallback: => enterCallback: =>
@selectFirstRow() @selectFirstRow()
...@@ -224,11 +243,19 @@ class GitLabDropdown ...@@ -224,11 +243,19 @@ class GitLabDropdown
noResults: -> noResults: ->
html = "<li>" html = "<li>"
html += "<a href='#' class='is-focused'>" html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
html += "No matching results." html += "No matching results."
html += "</a>" html += "</a>"
html += "</li>" html += "</li>"
highlightRow: (index) ->
if @input.val() isnt ""
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
$(selector).addClass 'is-focused'
rowClicked: (el) -> rowClicked: (el) ->
fieldName = @options.fieldName fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']") field = @dropdown.parent().find("input[name='#{fieldName}']")
...@@ -272,7 +299,7 @@ class GitLabDropdown ...@@ -272,7 +299,7 @@ class GitLabDropdown
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a" selector = ".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link # simulate a click on the first link
$(selector).trigger "click" $(selector).trigger "click"
$.fn.glDropdown = (opts) -> $.fn.glDropdown = (opts) ->
......
#= require jquery.waitforimages
class @IssuableContext class @IssuableContext
constructor: -> constructor: ->
@initParticipants()
new UsersSelect() new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
...@@ -17,3 +17,27 @@ class @IssuableContext ...@@ -17,3 +17,27 @@ class @IssuableContext
block.find('.js-select2').select2("open") block.find('.js-select2').select2("open")
$(".right-sidebar").niceScroll() $(".right-sidebar").niceScroll()
initParticipants: ->
_this = @
$(document).on "click", ".js-participants-more", @toggleHiddenParticipants
$(".js-participants-author").each (i) ->
if i >= _this.PARTICIPANTS_ROW_COUNT
$(@)
.addClass "js-participants-hidden"
.hide()
toggleHiddenParticipants: (e) ->
e.preventDefault()
currentText = $(this).text().trim()
lessText = $(this).data("less-text")
originalText = $(this).data("original-text")
if currentText is originalText
$(this).text(lessText)
else
$(this).text(originalText)
$(".js-participants-hidden").toggle()
...@@ -7,7 +7,6 @@ class @Issue ...@@ -7,7 +7,6 @@ class @Issue
# Prevent duplicate event bindings # Prevent duplicate event bindings
@disableTaskList() @disableTaskList()
@fixAffixScroll() @fixAffixScroll()
@initParticipants()
if $('a.btn-close').length if $('a.btn-close').length
@initTaskList() @initTaskList()
@initIssueBtnEventListeners() @initIssueBtnEventListeners()
...@@ -85,27 +84,3 @@ class @Issue ...@@ -85,27 +84,3 @@ class @Issue
type: 'PATCH' type: 'PATCH'
url: $('form.js-issuable-update').attr('action') url: $('form.js-issuable-update').attr('action')
data: patchData data: patchData
initParticipants: ->
_this = @
$(document).on "click", ".js-participants-more", @toggleHiddenParticipants
$(".js-participants-author").each (i) ->
if i >= _this.PARTICIPANTS_ROW_COUNT
$(@)
.addClass "js-participants-hidden"
.hide()
toggleHiddenParticipants: (e) ->
e.preventDefault()
currentText = $(this).text().trim()
lessText = $(this).data("less-text")
originalText = $(this).data("original-text")
if currentText is originalText
$(this).text(lessText)
else
$(this).text(originalText)
$(".js-participants-hidden").toggle()
...@@ -6,7 +6,7 @@ class @LabelsSelect ...@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl = $dropdown.data('labels') labelUrl = $dropdown.data('labels')
selectedLabel = $dropdown.data('selected') selectedLabel = $dropdown.data('selected')
if selectedLabel if selectedLabel
selectedLabel = selectedLabel.split(',') selectedLabel = selectedLabel.toString().split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
...@@ -14,18 +14,61 @@ class @LabelsSelect ...@@ -14,18 +14,61 @@ class @LabelsSelect
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
if newLabelField.length if newLabelField.length
$newLabelCreateButton = $('.js-new-label-btn')
$colorPreview = $('.js-dropdown-label-color-preview')
$newLabelError = $dropdown.parent().find('.js-label-error') $newLabelError = $dropdown.parent().find('.js-label-error')
$newLabelError.hide() $newLabelError.hide()
# Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on 'click', (e) -> $('.suggest-colors-dropdown a').on 'click', (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
newColorField.val $(this).data('color') newColorField
$('.js-dropdown-label-color-preview') .val($(this).data('color'))
.trigger('change')
$colorPreview
.css 'background-color', $(this).data('color') .css 'background-color', $(this).data('color')
.parent()
.addClass 'is-active' .addClass 'is-active'
$('.js-new-label-btn').on 'click', (e) -> # Cancel button takes back to first page
resetForm = ->
newLabelField
.val ''
.trigger 'change'
newColorField
.val ''
.trigger 'change'
$colorPreview
.css 'background-color', ''
.parent()
.removeClass 'is-active'
$('.dropdown-menu-back').on 'click', ->
resetForm()
$('.js-cancel-label-btn').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
resetForm()
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
# Listen for change and keyup events on label and color field
# This allows us to enable the button when ready
enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelCreateButton.enable()
else
$newLabelCreateButton.disable()
newLabelField.on 'keyup change', enableLabelCreateButton
newColorField.on 'keyup change', enableLabelCreateButton
# Send the API call to create the label
$newLabelCreateButton
.disable()
.on 'click', (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
...@@ -78,8 +121,11 @@ class @LabelsSelect ...@@ -78,8 +121,11 @@ class @LabelsSelect
else else
selected = if label.title is selectedLabel then 'is-active' else '' selected = if label.title is selectedLabel then 'is-active' else ''
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li> "<li>
<a href='#' class='#{selected}'> <a href='#' class='#{selected}'>
#{color}
#{label.title} #{label.title}
</a> </a>
</li>" </li>"
......
class @Profile class @Profile
constructor: -> constructor: (opts = {}) ->
{
@form = $('.edit-user')
} = opts
# Automatically submit the Preferences form when any of its radio buttons change # Automatically submit the Preferences form when any of its radio buttons change
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit() $(this).parents('form').submit()
...@@ -17,14 +21,46 @@ class @Profile ...@@ -17,14 +21,46 @@ class @Profile
$('.update-notifications').on 'ajax:complete', -> $('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
$('.js-choose-user-avatar-button').bind "click", -> @bindEvents()
form = $(this).closest("form")
form.find(".js-user-avatar-input").click() cropOpts =
filename: '.js-avatar-filename'
previewImage: '.avatar-image .avatar'
modalCrop: '.modal-profile-crop'
pickImageEl: '.js-choose-user-avatar-button'
uploadImageBtn: '.js-upload-user-avatar'
modalCropImg: '.modal-profile-crop-image'
@avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
bindEvents: ->
@form.on 'submit', @onSubmitForm
onSubmitForm: (e) =>
e.preventDefault()
@saveForm()
saveForm: ->
self = @
formData = new FormData(@form[0])
formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
$('.js-user-avatar-input').bind "change", -> $.ajax
form = $(this).closest("form") url: @form.attr('action')
filename = $(this).val().replace(/^.*[\\\/]/, '') type: @form.attr('method')
form.find(".js-avatar-filename").text(filename) data: formData
dataType: "json"
processData: false
contentType: false
success: (response) ->
new Flash(response.message, 'notice')
error: (jqXHR) ->
new Flash(jqXHR.responseJSON.message, 'alert')
complete: ->
window.scrollTo 0, 0
# Enable submit button after requests ends
self.form.find(':input[disabled]').enable()
$ -> $ ->
# Extract the SSH Key title from its comment # Extract the SSH Key title from its comment
......
...@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' ...@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = -> toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded") $('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
......
...@@ -30,6 +30,7 @@ class @UsersSelect ...@@ -30,6 +30,7 @@ class @UsersSelect
if showNullUser if showNullUser
showDivider += 1 showDivider += 1
users.unshift( users.unshift(
beforeDivider: true
name: 'Unassigned', name: 'Unassigned',
id: 0 id: 0
) )
...@@ -39,6 +40,7 @@ class @UsersSelect ...@@ -39,6 +40,7 @@ class @UsersSelect
name = showAnyUser name = showAnyUser
name = 'Any User' if name == true name = 'Any User' if name == true
anyUser = { anyUser = {
beforeDivider: true
name: name, name: name,
id: null id: null
} }
...@@ -75,6 +77,13 @@ class @UsersSelect ...@@ -75,6 +77,13 @@ class @UsersSelect
selected = if user.id is selectedId then "is-active" else "" selected = if user.id is selectedId then "is-active" else ""
img = "" img = ""
if user.beforeDivider?
"<li>
<a href='#' class='#{selected}'>
#{user.name}
</a>
</li>"
else
if avatar if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
*= require_self *= require_self
*= require dropzone/basic *= require dropzone/basic
*= require cal-heatmap *= require cal-heatmap
*= require cropper.css
*/ */
/* /*
......
...@@ -301,8 +301,11 @@ table { ...@@ -301,8 +301,11 @@ table {
} }
.btn-sign-in { .btn-sign-in {
margin-top: 10px;
text-shadow: none; text-shadow: none;
@media (min-width: $screen-sm-min) {
margin-top: 11px;
}
} }
.side-filters { .side-filters {
...@@ -384,6 +387,7 @@ table { ...@@ -384,6 +387,7 @@ table {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
min-width: 250px;
visibility: hidden; visibility: hidden;
} }
} }
......
...@@ -130,6 +130,12 @@ ...@@ -130,6 +130,12 @@
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
} }
&.dropdown-menu-empty-link {
&.is-focused {
background-color: $dropdown-empty-row-bg;
}
}
} }
} }
...@@ -183,7 +189,7 @@ ...@@ -183,7 +189,7 @@
} }
.dropdown-select { .dropdown-select {
width: 280px; width: 300px;
} }
.dropdown-menu-align-right { .dropdown-menu-align-right {
...@@ -237,7 +243,7 @@ ...@@ -237,7 +243,7 @@
.dropdown-title-button { .dropdown-title-button {
position: absolute; position: absolute;
top: -1px; top: 0;
padding: 0; padding: 0;
color: $dropdown-title-btn-color; color: $dropdown-title-btn-color;
font-size: 14px; font-size: 14px;
...@@ -270,6 +276,22 @@ ...@@ -270,6 +276,22 @@
font-size: 12px; font-size: 12px;
pointer-events: none; pointer-events: none;
} }
.dropdown-input-clear {
display: none;
cursor: pointer;
pointer-events: all;
}
&.has-value {
.dropdown-input-clear {
display: block;
}
.dropdown-input-search {
display: none;
}
}
} }
.dropdown-input-field { .dropdown-input-field {
...@@ -286,13 +308,13 @@ ...@@ -286,13 +308,13 @@
border-color: $dropdown-input-focus-border; border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow; box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa { ~ .fa {
color: $dropdown-link-color; color: $dropdown-link-color;
} }
} }
&:hover { &:hover {
+ .fa { ~ .fa {
color: $dropdown-link-color; color: $dropdown-link-color;
} }
} }
...@@ -338,11 +360,12 @@ ...@@ -338,11 +360,12 @@
} }
} }
.dropdown-menu-labels { .dropdown-label-box {
.label {
position: relative; position: relative;
width: 30px; top: 3px;
margin-right: 5px; margin-right: 5px;
text-indent: -99999px; display: inline-block;
} width: 15px;
height: 15px;
border-radius: $border-radius-base;
} }
...@@ -50,6 +50,10 @@ ...@@ -50,6 +50,10 @@
} }
} }
a {
color: $gl-dark-link-color;
}
.left-options { .left-options {
margin-top: -3px; margin-top: -3px;
} }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.filter-item { .filter-item {
display: block; display: block;
margin: 0 0 10px 0; margin: 0 0 10px;
} }
} }
// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
// the way the `src` property is formatted in this file.
// scss-lint:disable SpaceAfterPropertyColon
/* latin-ext */ /* latin-ext */
@font-face { @font-face {
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
......
...@@ -70,6 +70,11 @@ header { ...@@ -70,6 +70,11 @@ header {
.header-content { .header-content {
height: $header-height; height: $header-height;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
.title { .title {
margin: 0; margin: 0;
......
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
padding-bottom: 0; padding-bottom: 0;
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control { .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px 0; margin: 0 0 10px;
display: block; display: block;
width: 100%; width: 100%;
} }
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
@include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
@include border-radius ($border-radius-default); @include border-radius ($border-radius-default);
border: none; border: none;
min-width: 175px;
} }
.select2-results .select2-result-label { .select2-results .select2-result-label {
......
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
}
.page-with-sidebar { .page-with-sidebar {
padding-top: $header-height; padding-top: $header-height;
transition-duration: .3s; transition-duration: .3s;
...@@ -18,24 +25,6 @@ ...@@ -18,24 +25,6 @@
position: absolute; position: absolute;
left: 0; left: 0;
} }
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
}
&.right-sidebar-expanded {
/* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
padding-right: $gutter_width;
}
}
} }
.sidebar-wrapper { .sidebar-wrapper {
...@@ -202,53 +191,27 @@ ...@@ -202,53 +191,27 @@
} }
} }
@mixin expanded-sidebar { .collapse-nav a {
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) */
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
}
}
.sidebar-wrapper {
width: $sidebar_width;
.nav-sidebar {
width: $sidebar_width; width: $sidebar_width;
} position: fixed;
bottom: 0;
.nav-sidebar li a{ left: 0;
width: 230px; font-size: 13px;
background: transparent;
height: 40px;
text-align: center;
line-height: 40px;
transition-duration: .3s;
outline: none;
&.back-link { &:hover {
i { text-decoration: none;
opacity: 0;
}
}
}
} }
} }
@mixin collapsed-sidebar { .page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width; padding-left: $sidebar_collapsed_width;
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
}
}
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_collapsed_width; width: $sidebar_collapsed_width;
...@@ -293,35 +256,48 @@ ...@@ -293,35 +256,48 @@
} }
} }
.collapse-nav a { .page-sidebar-expanded {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
.sidebar-wrapper {
width: $sidebar_width; width: $sidebar_width;
position: fixed;
bottom: 0;
left: 0;
font-size: 13px;
background: transparent;
height: 40px;
text-align: center;
line-height: 40px;
transition-duration: .3s;
outline: none;
}
.collapse-nav a:hover { .nav-sidebar {
text-decoration: none; width: $sidebar_width;
background: #f2f6f7; }
.nav-sidebar li a {
width: 230px;
&.back-link {
i {
opacity: 0;
}
}
}
}
} }
.page-sidebar-collapsed { .right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
@include collapsed-sidebar;
padding-right: 0; padding-right: 0;
/* Small devices (tablets, 768px and up) */
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
@include collapsed-sidebar; padding-right: $sidebar_collapsed_width;
} }
} }
.page-sidebar-expanded { .right-sidebar-expanded {
@include expanded-sidebar; padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
padding-right: $sidebar_collapsed_width;
}
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
}
} }
...@@ -168,13 +168,14 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif ...@@ -168,13 +168,14 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
*/ */
$dropdown-bg: #fff; $dropdown-bg: #fff;
$dropdown-link-color: #555; $dropdown-link-color: #555;
$dropdown-link-hover-bg: rgba(#000, .04); $dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
$dropdown-border-color: rgba(#000, .1); $dropdown-border-color: rgba(#000, .1);
$dropdown-shadow-color: rgba(#000, .1); $dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494; $dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf; $dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #c7c7c7; $dropdown-input-color: #555;
$dropdown-input-focus-border: rgb(58, 171, 240); $dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2); $dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6); $dropdown-loading-bg: rgba(#fff, .6);
......
...@@ -5,8 +5,8 @@ table.code { ...@@ -5,8 +5,8 @@ table.code {
font-family: $monospace_font; font-family: $monospace_font;
border: none; border: none;
border-collapse: separate; border-collapse: separate;
margin: 0px; margin: 0;
padding: 0px; padding: 0;
-premailer-cellpadding: 0; -premailer-cellpadding: 0;
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
-premailer-width: 100%; -premailer-width: 100%;
...@@ -18,12 +18,12 @@ table.code { ...@@ -18,12 +18,12 @@ table.code {
} }
td.diff-line-num { td.diff-line-num {
margin: 0px; margin: 0;
padding: 0px; padding: 0;
border: none; border: none;
background: $background-color; background: $background-color;
color: rgba(0, 0, 0, 0.3); color: rgba(0, 0, 0, 0.3);
padding: 0px 5px; padding: 0 5px;
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
text-align: right; text-align: right;
min-width: 35px; min-width: 35px;
...@@ -33,8 +33,8 @@ table.code { ...@@ -33,8 +33,8 @@ table.code {
td.line_content { td.line_content {
display: block; display: block;
margin: 0px; margin: 0;
padding: 0px 0.5em; padding: 0 0.5em;
border: none; border: none;
white-space: pre; white-space: pre;
} }
......
...@@ -3,12 +3,12 @@ img { ...@@ -3,12 +3,12 @@ img {
height: auto; height: auto;
} }
p.details { p.details {
font-style:italic; font-style: italic;
color:#777 color: #777
} }
.footer p { .footer p {
font-size:small; font-size: small;
color:#777 color: #777
} }
pre.commit-message { pre.commit-message {
white-space: pre-wrap; white-space: pre-wrap;
...@@ -20,5 +20,5 @@ pre.commit-message { ...@@ -20,5 +20,5 @@ pre.commit-message {
color: #090; color: #090;
} }
.file-stats .deleted-file { .file-stats .deleted-file {
color: #B00; color: #b00;
} }
...@@ -99,6 +99,10 @@ li.commit { ...@@ -99,6 +99,10 @@ li.commit {
color: $gl-gray; color: $gl-gray;
} }
.avatar {
margin-right: 8px;
}
.committed_ago { .committed_ago {
display: inline-block; display: inline-block;
} }
......
...@@ -9,28 +9,45 @@ ...@@ -9,28 +9,45 @@
} }
&.suggest-colors-dropdown { &.suggest-colors-dropdown {
margin-bottom: 5px; margin-top: 10px;
margin-bottom: 10px;
border-radius: $border-radius-base;
overflow: hidden;
a { a {
@include border-radius(0); @include border-radius(0);
width: 36.7px; width: (100% / 7);
margin-right: 0; margin-right: 0;
margin-bottom: -5px; margin-bottom: -5px;
} }
} }
} }
.dropdown-label-color-preview { .dropdown-new-label {
display: none; .dropdown-content {
margin-top: 5px; max-height: 260px;
width: 100%; }
height: 25px; }
.dropdown-label-color-input {
position: relative;
margin-bottom: 10px;
&.is-active { &.is-active {
display: block; padding-left: 32px;
} }
} }
.dropdown-label-color-preview {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
border-top-left-radius: $border-radius-base;
border-bottom-left-radius: $border-radius-base;
}
.label-row { .label-row {
.label { .label {
padding: 9px; padding: 9px;
......
...@@ -197,3 +197,24 @@ ...@@ -197,3 +197,24 @@
width: 105px; width: 105px;
} }
} }
.modal-profile-crop {
.modal-dialog {
width: 380px;
@media (max-width: $screen-sm-min) {
width: auto;
}
}
.profile-crop-image-container {
height: 300px;
margin: 0 auto;
}
.crop-controls {
padding: 10px 0 0;
text-align: center;
}
}
...@@ -229,6 +229,10 @@ ...@@ -229,6 +229,10 @@
padding: 0 3px; padding: 0 3px;
color: #999; color: #999;
} }
a {
color: $gl-dark-link-color;
}
} }
.last-push-widget { .last-push-widget {
......
.ci-status { .container-fluid .content {
.ci-status {
padding: 2px 7px; padding: 2px 7px;
margin-right: 5px; margin-right: 5px;
border: 1px solid #eee; border: 1px solid #eee;
...@@ -24,6 +25,8 @@ ...@@ -24,6 +25,8 @@
border-color: $gl-info; border-color: $gl-info;
} }
&.ci-canceled,
&.ci-skipped,
&.ci-disabled { &.ci-disabled {
color: $gl-gray; color: $gl-gray;
border-color: $gl-gray; border-color: $gl-gray;
...@@ -34,21 +37,22 @@ ...@@ -34,21 +37,22 @@
color: $gl-warning; color: $gl-warning;
border-color: $gl-warning; border-color: $gl-warning;
} }
} }
.ci-status-icon-success { .ci-status-icon-success {
@extend .cgreen; color: $gl-success;
} }
.ci-status-icon-failed { .ci-status-icon-failed {
@extend .cred; color: $gl-danger;
} }
.ci-status-icon-running, .ci-status-icon-running,
.ci-status-icon-pending { .ci-status-icon-pending {
// These are standard text color color: $gl-warning;
} }
.ci-status-icon-canceled, .ci-status-icon-canceled,
.ci-status-icon-disabled, .ci-status-icon-disabled,
.ci-status-icon-not-found, .ci-status-icon-not-found,
.ci-status-icon-skipped { .ci-status-icon-skipped {
@extend .cgray; color: $gl-gray;
}
} }
...@@ -6,7 +6,6 @@ module GlobalMilestones ...@@ -6,7 +6,6 @@ module GlobalMilestones
@milestones = MilestonesFinder.new.execute(@projects, params) @milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones) @milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
@milestones = Kaminari.paginate_array(@milestones).page(params[:page])
end end
def milestone def milestone
......
class Dashboard::ApplicationController < ApplicationController class Dashboard::ApplicationController < ApplicationController
layout 'dashboard' layout 'dashboard'
private
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end end
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
respond_to do |format|
format.json { render json: labels }
end
end
end
...@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController ...@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones include GlobalMilestones
before_action :projects before_action :projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show] before_action :milestone, only: [:show]
def index def index
respond_to do |format|
format.html do
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
render json: milestones
end
end end
def show
end end
private def show
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end end
end end
...@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
include MergeRequestsAction include MergeRequestsAction
before_action :event_filter, only: :activity before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests, :labels, :milestones] before_action :projects, only: [:issues, :merge_requests]
respond_to :html respond_to :html
...@@ -20,29 +20,6 @@ class DashboardController < Dashboard::ApplicationController ...@@ -20,29 +20,6 @@ class DashboardController < Dashboard::ApplicationController
end end
end end
def labels
labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title)
respond_to do |format|
format.json do
render json: labels
end
end
end
def milestones
milestones = Milestone.where(project_id: @projects).active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
respond_to do |format|
format.json do
render json: grouped_milestones
end
end
end
protected protected
def load_events def load_events
...@@ -57,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController ...@@ -57,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations @events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end end
...@@ -2,11 +2,15 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -2,11 +2,15 @@ class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones include GlobalMilestones
before_action :group_projects before_action :group_projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show, :update] before_action :milestone, only: [:show, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index def index
respond_to do |format|
format.html do
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
end
end end
def new def new
......
...@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController ...@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController
def update def update
user_params.except!(:email) if @user.ldap_user? user_params.except!(:email) if @user.ldap_user?
respond_to do |format|
if @user.update_attributes(user_params) if @user.update_attributes(user_params)
flash[:notice] = "Profile was successfully updated" message = "Profile was successfully updated"
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
format.json { render json: { message: message } }
else else
messages = @user.errors.full_messages.uniq.join('. ') message = @user.errors.full_messages.uniq.join('. ')
flash[:alert] = "Failed to update profile. #{messages}" format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
format.json { render json: { message: message }, status: :unprocessable_entity }
end end
respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'show' }) }
end end
end end
......
...@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController ...@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController
redirect_to activity_dashboard_path redirect_to activity_dashboard_path
when 'starred_project_activity' when 'starred_project_activity'
redirect_to activity_dashboard_path(filter: 'starred') redirect_to activity_dashboard_path(filter: 'starred')
when 'groups'
redirect_to dashboard_groups_path
when 'todos'
redirect_to dashboard_todos_path
else else
return return
end end
......
...@@ -70,7 +70,8 @@ module DropdownsHelper ...@@ -70,7 +70,8 @@ module DropdownsHelper
def dropdown_filter(placeholder) def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search') filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
filter_output.html_safe filter_output.html_safe
end end
......
...@@ -194,7 +194,7 @@ module EventsHelper ...@@ -194,7 +194,7 @@ module EventsHelper
end end
def event_to_atom(xml, event) def event_to_atom(xml, event)
if event.proper?(current_user) if event.visible_to_user?(current_user)
xml.entry do xml.entry do
event_link = event_feed_url(event) event_link = event_feed_url(event)
event_title = event_feed_title(event) event_title = event_feed_title(event)
......
...@@ -114,7 +114,7 @@ module LabelsHelper ...@@ -114,7 +114,7 @@ module LabelsHelper
if @project if @project
namespace_project_labels_path(@project.namespace, @project, :json) namespace_project_labels_path(@project.namespace, @project, :json)
else else
labels_dashboard_path(:json) dashboard_labels_path(:json)
end end
end end
......
...@@ -50,7 +50,7 @@ module MilestonesHelper ...@@ -50,7 +50,7 @@ module MilestonesHelper
if @project if @project
namespace_project_milestones_path(@project.namespace, @project, :json) namespace_project_milestones_path(@project.namespace, @project, :json)
else else
milestones_dashboard_path(:json) dashboard_milestones_path(:json)
end end
end end
......
...@@ -12,7 +12,9 @@ module PreferencesHelper ...@@ -12,7 +12,9 @@ module PreferencesHelper
projects: 'Your Projects (default)', projects: 'Your Projects (default)',
stars: 'Starred Projects', stars: 'Starred Projects',
project_activity: "Your Projects' Activity", project_activity: "Your Projects' Activity",
starred_project_activity: "Starred Projects' Activity" starred_project_activity: "Starred Projects' Activity",
groups: "Your Groups",
todos: "Your Todos"
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text # Returns an Array usable by a select field for more user-friendly option text
......
...@@ -230,7 +230,7 @@ class Commit ...@@ -230,7 +230,7 @@ class Commit
end end
def revert_message def revert_message
%Q{Revert "#{title}"\n\n#{revert_description}} %Q{Revert "#{title.strip}"\n\n#{revert_description}}
end end
def reverts_commit?(commit) def reverts_commit?(commit)
......
...@@ -41,7 +41,7 @@ module Issuable ...@@ -41,7 +41,7 @@ module Issuable
scope :join_project, -> { joins(:project) } scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) } scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.merge(Project.non_archived) } scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
delegate :name, delegate :name,
:email, :email,
......
...@@ -79,15 +79,15 @@ class Event < ActiveRecord::Base ...@@ -79,15 +79,15 @@ class Event < ActiveRecord::Base
end end
end end
def proper?(user = nil) def visible_to_user?(user = nil)
if push? if push?
true true
elsif membership_changed? elsif membership_changed?
true true
elsif created_project? elsif created_project?
true true
elsif issue? elsif issue? || issue_note?
Ability.abilities.allowed?(user, :read_issue, issue) Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
else else
((merge_request? || note?) && target) || milestone? ((merge_request? || note?) && target) || milestone?
end end
...@@ -304,6 +304,10 @@ class Event < ActiveRecord::Base ...@@ -304,6 +304,10 @@ class Event < ActiveRecord::Base
target.noteable_type == "Commit" target.noteable_type == "Commit"
end end
def issue_note?
note? && target && target.noteable_type == "Issue"
end
def note_project_snippet? def note_project_snippet?
target.noteable_type == "Snippet" target.noteable_type == "Snippet"
end end
......
...@@ -156,7 +156,8 @@ class Issue < ActiveRecord::Base ...@@ -156,7 +156,8 @@ class Issue < ActiveRecord::Base
return false unless user.can?(:admin_issue, to_project) return false unless user.can?(:admin_issue, to_project)
end end
!moved? && user.can?(:admin_issue, self.project) !moved? && persisted? &&
user.can?(:admin_issue, self.project)
end end
def to_branch_name def to_branch_name
......
...@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base ...@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base
end end
end end
def open_issues_count def open_issues_count(user = nil)
issues.opened.count issues.visible_to_user(user).opened.count
end end
def closed_issues_count def closed_issues_count(user = nil)
issues.closed.count issues.visible_to_user(user).closed.count
end end
def open_merge_requests_count def open_merge_requests_count
......
...@@ -84,7 +84,7 @@ class Milestone < ActiveRecord::Base ...@@ -84,7 +84,7 @@ class Milestone < ActiveRecord::Base
end end
def self.upcoming def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
end end
def to_reference(from_project = nil) def to_reference(from_project = nil)
......
...@@ -339,7 +339,7 @@ class Project < ActiveRecord::Base ...@@ -339,7 +339,7 @@ class Project < ActiveRecord::Base
end end
def find_with_namespace(id) def find_with_namespace(id)
namespace_path, project_path = id.split('/') namespace_path, project_path = id.split('/', 2)
return nil if !namespace_path || !project_path return nil if !namespace_path || !project_path
......
...@@ -505,6 +505,18 @@ class Repository ...@@ -505,6 +505,18 @@ class Repository
end end
end end
def gitlab_ci_yml
return nil if !exists? || empty?
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
rescue Rugged::ReferenceError
# For unknow reason spinach scenario "Scenario: I change project path"
# lead to "Reference 'HEAD' not found" exception from Repository#empty?
nil
end
def head_commit def head_commit
@head_commit ||= commit(self.root_ref) @head_commit ||= commit(self.root_ref)
end end
...@@ -1021,6 +1033,8 @@ class Repository ...@@ -1021,6 +1033,8 @@ class Repository
end end
def avatar def avatar
return nil unless exists?
@avatar ||= cache.fetch(:avatar) do @avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file| AVATAR_FILES.find do |file|
blob_at_branch('master', file) blob_at_branch('master', file)
......
...@@ -186,7 +186,7 @@ class User < ActiveRecord::Base ...@@ -186,7 +186,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference # User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity] enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
# User's Project preference # User's Project preference
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
......
...@@ -54,7 +54,8 @@ module Issues ...@@ -54,7 +54,8 @@ module Issues
new_note = note.dup new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue, new_params = { project: @new_project, noteable: @new_issue,
note: unfold_references(new_note.note), note: unfold_references(new_note.note),
created_at: note.created_at } created_at: note.created_at,
updated_at: note.updated_at }
new_note.update(new_params) new_note.update(new_params)
end end
...@@ -78,6 +79,8 @@ module Issues ...@@ -78,6 +79,8 @@ module Issues
end end
def unfold_references(content) def unfold_references(content)
return unless content
rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project, rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
@current_user) @current_user)
rewriter.rewrite(@new_project) rewriter.rewrite(@new_project)
......
...@@ -162,6 +162,7 @@ class NotificationService ...@@ -162,6 +162,7 @@ class NotificationService
recipients = add_subscribed_users(recipients, note.noteable) recipients = add_subscribed_users(recipients, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable) recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author) recipients.delete(note.author)
recipients = recipients.uniq recipients = recipients.uniq
...@@ -376,6 +377,14 @@ class NotificationService ...@@ -376,6 +377,14 @@ class NotificationService
end end
end end
def reject_users_without_access(recipients, target)
return recipients unless target.is_a?(Issue)
recipients.select do |user|
user.can?(:read_issue, target)
end
end
def add_subscribed_users(recipients, target) def add_subscribed_users(recipients, target)
return recipients unless target.respond_to? :subscribers return recipients unless target.respond_to? :subscribers
...@@ -464,15 +473,16 @@ class NotificationService ...@@ -464,15 +473,16 @@ class NotificationService
end end
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) recipients.delete(current_user)
recipients.uniq recipients.uniq
end end
def build_relabeled_recipients(target, current_user, labels:) def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels) recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) recipients.delete(current_user)
recipients.uniq recipients.uniq
end end
......
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
%em Authorization was granted by entering your username and password in the application. %em Authorization was granted by entering your username and password in the application.
%td= token.created_at %td= token.created_at
%td= token.scopes %td= token.scopes
%td= render 'delete_form', token: token %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- else - else
.profile-settings-message.text-center .profile-settings-message.text-center
You don't have any authorized applications You don't have any authorized applications
- if event.proper?(current_user) - if event.visible_to_user?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= icon('bars') = icon('bars')
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav.pull-right %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render 'layouts/search' = render 'layouts/search'
%li.visible-sm.visible-xs %li.visible-sm.visible-xs
...@@ -38,7 +38,8 @@ ...@@ -38,7 +38,8 @@
= link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('sign-out') = icon('sign-out')
- else - else
.pull-right %li
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
......
...@@ -32,4 +32,5 @@ ...@@ -32,4 +32,5 @@
= f.password_field :password_confirmation, required: true, class: 'form-control' = f.password_field :password_confirmation, required: true, class: 'form-control'
.prepend-top-default.append-bottom-default .prepend-top-default.append-bottom-default
= f.submit 'Save password', class: "btn btn-create append-right-10" = f.submit 'Save password', class: "btn btn-create append-right-10"
- unless @user.password_automatically_set?
= link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link" = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
%a.btn.js-choose-user-avatar-button %a.btn.js-choose-user-avatar-button
Browse file... Browse file...
%span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
= f.file_field :avatar, class: "js-user-avatar-input hidden" = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*"
.help-block .help-block
The maximum file size allowed is 200KB. The maximum file size allowed is 200KB.
- if @user.avatar? - if @user.avatar?
...@@ -94,3 +94,25 @@ ...@@ -94,3 +94,25 @@
.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
Position and size your new avatar
.modal-body
.profile-crop-image-container
%img.modal-profile-crop-image
.crop-controls
.btn-group
%button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
%span.fa.fa-search-plus
%button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
%span.fa.fa-search-minus
.modal-footer
%button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
Set new profile picture
%fieldset.builds-feature %fieldset.builds-feature
%legend %legend
Builds: Builds:
- unless @repository.gitlab_ci_yml
.form-group
.col-sm-offset-2.col-sm-10
%p Builds need to be configured before you can begin using Continuous Integration.
= link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
%hr
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
%p Get recent application code using the following command: %p Get recent application code using the following command:
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
- else - else
Name Name
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to namespace_project_branches_path(sort: nil) do = link_to namespace_project_branches_path(sort: nil) do
Name Name
......
...@@ -27,6 +27,9 @@ ...@@ -27,6 +27,9 @@
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench') = icon('wrench')
%span CI Lint %span CI Lint
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
.panel-heading .panel-heading
Commits (#{@commits.count}) Commits (#{@commits.count})
- if hidden > 0 - if hidden > 0
%ul.well-list %ul.content-list
- commits.each do |commit| - commits.each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project = render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled %li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else - else
%ul.well-list= render commits, project: @project %ul.content-list= render commits, project: @project
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.light .light
= pluralize(commits.count, 'commit') = pluralize(commits.count, 'commit')
.col-md-10.col-sm-12 .col-md-10.col-sm-12
%ul.bordered-list %ul.content-list
= render commits, project: project = render commits, project: project
%hr.lists-separator %hr.lists-separator
......
- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) - if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
.pull-right .pull-right
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name do
= icon('code-fork') = icon('code-fork')
New Branch New Branch
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%strong.append-right-20 %strong.append-right-20
= link_to_label(label) do = link_to_label(label) do
= pluralize label.open_issues_count, 'open issue' = pluralize label.open_issues_count(current_user), 'open issue'
- if current_user - if current_user
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%span.caret %span.caret
%span.sr-only %span.sr-only
Select Archive Format Select Archive Format
%ul.col-xs-10.dropdown-menu{ role: 'menu' } %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download %i.fa.fa-download
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= icon('users') = icon('users')
= number_with_delimiter(group.users.count) = number_with_delimiter(group.users.count)
%span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false) = visibility_level_icon(group.visibility_level, fw: false)
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
......
...@@ -43,17 +43,17 @@ ...@@ -43,17 +43,17 @@
.issues_bulk_update.hide .issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline .filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul %ul
%li %li
%a{href: "#", data: {id: "reopen"}} Open %a{href: "#", data: {id: "reopen"}} Open
%li %li
%a{href: "#", data: {id: "close"}} Closed %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline .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", = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", 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), null_user: true, 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 .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event] = hidden_field_tag :state_event, params[:state_event]
......
...@@ -174,7 +174,7 @@ ...@@ -174,7 +174,7 @@
for this project. for this project.
- if issuable.new_record? - if issuable.new_record?
= link_to 'Cancel', namespace_project_issues_path(@project.namespace, @project), class: 'btn btn-cancel' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else - else
.pull-right .pull-right
- if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
...@@ -182,7 +182,7 @@ ...@@ -182,7 +182,7 @@
method: :delete, class: 'btn btn-grouped' do method: :delete, class: 'btn btn-grouped' do
= icon('trash-o') = icon('trash-o')
Delete Delete
= link_to 'Cancel', namespace_project_issue_path(@project.namespace, @project, issuable), class: 'btn btn-grouped btn-cancel' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
%li.project-approvers.hide.approver-template{id: "user_{user_id}"} %li.project-approvers.hide.approver-template{id: "user_{user_id}"}
= link_to "{approver_name}", "#" = link_to "{approver_name}", "#"
......
...@@ -24,17 +24,21 @@ ...@@ -24,17 +24,21 @@
- else - else
View labels View labels
- if can? current_user, :admin_label, @project and @project - if can? current_user, :admin_label, @project and @project
.dropdown-page-two .dropdown-page-two.dropdown-new-label
= dropdown_title("Create new label", back: true) = dropdown_title("Create new label", back: true)
= dropdown_content do = dropdown_content do
.dropdown-labels-error.js-label-error .dropdown-labels-error.js-label-error
%input#new_label_color{type: "hidden"}
%input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
.dropdown-label-color-preview.js-dropdown-label-color-preview
.suggest-colors.suggest-colors-dropdown .suggest-colors.suggest-colors-dropdown
- suggested_colors.each do |color| - suggested_colors.each do |color|
= link_to '#', style: "background-color: #{color}", data: { color: color } do = link_to '#', style: "background-color: #{color}", data: { color: color } do
&nbsp &nbsp
%button.btn.btn-primary.js-new-label-btn{type: "button"} .dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
%input#new_label_color.dropdown-input-field{ type: "text" }
.clearfix
%button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"}
Create Create
%button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"}
Cancel
= dropdown_loading = dropdown_loading
...@@ -17,4 +17,4 @@ ...@@ -17,4 +17,4 @@
%a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}} %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ #{participants_extra} more + #{participants_extra} more
:javascript :javascript
Issue.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row}; IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
...@@ -33,11 +33,11 @@ ...@@ -33,11 +33,11 @@
.value.bold.hide-collapsed .value.bold.hide-collapsed
- if issuable.assignee - if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 32) do = link_to_member(@project, issuable.assignee, size: 32) do
%span.username
= issuable.assignee.to_reference
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee) - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'} %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle') = icon('exclamation-triangle')
%span.username
= issuable.assignee.to_reference
- else - else
.light None .light None
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
Labels Labels
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)} .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
- if issuable.labels.any? - if issuable.labels.any?
- issuable.labels.each do |label| - issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name) = link_to_label(label, type: issuable.to_ability_name)
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
%span %span
= icon('star') = icon('star')
= project.star_count = project.star_count
%span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
= visibility_level_icon(project.visibility_level, fw: false) = visibility_level_icon(project.visibility_level, fw: false)
.title .title
......
...@@ -16,6 +16,18 @@ Rails.application.routes.draw do ...@@ -16,6 +16,18 @@ Rails.application.routes.draw do
end end
end end
# Make the built-in Rails routes available in development, otherwise they'd
# get swallowed by the `namespace/project` route matcher below.
#
# See https://git.io/va79N
if Rails.env.development?
get '/rails/mailers' => 'rails/mailers#index'
get '/rails/mailers/:path' => 'rails/mailers#preview'
get '/rails/info/properties' => 'rails/info#properties'
get '/rails/info/routes' => 'rails/info#routes'
get '/rails/info' => 'rails/info#index'
end
namespace :ci do namespace :ci do
# CI API # CI API
Ci::API::API.logger Rails.logger Ci::API::API.logger Rails.logger
...@@ -368,11 +380,10 @@ Rails.application.routes.draw do ...@@ -368,11 +380,10 @@ Rails.application.routes.draw do
get :issues get :issues
get :merge_requests get :merge_requests
get :activity get :activity
get :labels
get :milestones
scope module: :dashboard do scope module: :dashboard do
resources :milestones, only: [:index, :show] resources :milestones, only: [:index, :show]
resources :labels, only: [:index]
resources :groups, only: [:index] resources :groups, only: [:index]
resources :snippets, only: [:index] resources :snippets, only: [:index]
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
## User documentation ## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API. - [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI](ci/README.md) - [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md). - [Importing to GitLab](workflow/importing/README.md).
...@@ -60,4 +60,3 @@ ...@@ -60,4 +60,3 @@
contributing to documentation. contributing to documentation.
- [Development](development/README.md) Explains the architecture and the guidelines for shell commands. - [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
- [Legal](legal/README.md) Contributor license agreements. - [Legal](legal/README.md) Contributor license agreements.
- [Release](release/README.md) How to make the monthly and security releases.
...@@ -72,9 +72,9 @@ p { margin: 0; padding: 0; } ...@@ -72,9 +72,9 @@ p { margin: 0; padding: 0; }
### Colors ### Colors
HEX (hexadecimal) colors short-form should use shortform where possible, and HEX (hexadecimal) colors should use shorthand where possible, and should use
should use lower case letters to differenciate between letters and numbers, e. lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3`
g. `#E3E3E3` vs. `#e3e3e3`. vs. `#e3e3e3`.
```scss ```scss
// Bad // Bad
...@@ -160,6 +160,7 @@ is slightly more performant. ...@@ -160,6 +160,7 @@ is slightly more performant.
``` ```
### Selectors with a `js-` Prefix ### Selectors with a `js-` Prefix
Do not use any selector prefixed with `js-` for styling purposes. These 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 selectors are intended for use only with JavaScript to allow for removal or
renaming without breaking styling. renaming without breaking styling.
...@@ -187,8 +188,28 @@ CSSComb globally (system-wide). Run it in the GitLab directory with ...@@ -187,8 +188,28 @@ CSSComb globally (system-wide). Run it in the GitLab directory with
Note that this won't fix every problem, but it should fix a majority. Note that this won't fix every problem, but it should fix a majority.
### Ignoring issues
If you want a line or set of lines to be ignored by the linter, you can use
`// scss-lint:disable RuleName` ([more info][disabling-linters]):
```scss
// This lint rule is disabled because the class name comes from a gem.
// scss-lint:disable SelectorFormat
.ui_charcoal {
background-color: #333;
}
// scss-lint:enable SelectorFormat
```
Make sure a comment is added on the line above the `disable` rule, otherwise the
linter will throw a warning. `DisableLinterReason` is enabled to make sure the
style guide isn't being ignored, and to communicate to others why the style
guide is ignored in this instance.
[csscomb]: https://github.com/csscomb/csscomb.js [csscomb]: https://github.com/csscomb/csscomb.js
[node]: https://github.com/nodejs/node [node]: https://github.com/nodejs/node
[npm]: https://www.npmjs.com/ [npm]: https://www.npmjs.com/
[scss-lint]: https://github.com/brigade/scss-lint [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 [scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
[disabling-linters]: https://github.com/brigade/scss-lint#disabling-linters-via-source
## Release cycle
Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
## Release process documentation
- [Monthly release](monthly.md), every month on the 22nd.
- [Patch release](patch.md), if there are serious regressions.
- [Security](security.md), for security problems.
- [Master](master.md), update process for the master branch.
# How to create RC1
The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub.
### 1. Update the installation guide
1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay)
1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782)
1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794)
1. There might be other changes. Ask around.
### 2. Create update guides
[Follow this guide](howto_update_guides.md) to create update guides.
### 3. Code quality indicators
Make sure the code quality indicators are green / good.
- [![Build status](http://ci.gitlab.org/projects/1/status.png?ref=master)](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch)
- [![Build Status](https://semaphoreapp.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/243338/badge.png)](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch)
- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.png)](https://codeclimate.com/github/gitlabhq/gitlabhq)
- [![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.png)](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available)
- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq)
### 4. Run release tool
**Make sure EE `master` has latest changes from CE `master`**
Get release tools
```
git clone git@dev.gitlab.org:gitlab/release-tools.git
cd release-tools
```
Release candidate creates stable branch from master.
So we need to sync master branch between all CE, EE and CI remotes.
```
bundle exec rake sync
```
Create release candidate and stable branch:
```
bundle exec rake release["x.x.0.rc1"]
```
Now developers can use master for merging new features.
So you should use stable branch for future code changes related to release.
# Create update guides
1. Create: CE update guide from previous version. Like `7.3-to-7.4.md`
1. Create: CE to EE update guide in EE repository for latest version.
1. Update: `6.x-or-7.x-to-7.x.md` to latest version.
1. Create: CI update guide from previous version
It's best to copy paste the previous guide and make changes where necessary.
The typical steps are listed below with any points you should specifically look at.
#### 0. Any major changes?
List any major changes here, so the user is aware of them before starting to upgrade. For instance:
- Database updates
- Web server changes
- File structure changes
#### 1. Stop server
#### 2. Make backup
#### 3. Do users need to update dependencies like `git`?
- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release.
- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release.
#### 4. Get latest code
#### 5. Does GitLab shell need to be updated?
#### 6. Install libs, migrations, etc.
#### 7. Any config files updated since last release?
Check if any of these changed since last release:
- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab)
- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl)
- <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example>
- [config/gitlab.yml.example](/config/gitlab.yml.example)
- [config/unicorn.rb.example](/config/unicorn.rb.example)
- [config/database.yml.mysql](/config/database.yml.mysql)
- [config/database.yml.postgresql](/config/database.yml.postgresql)
- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example)
- [config/resque.yml.example](/config/resque.yml.example)
#### 8. Need to update init script?
Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab)
#### 9. Start application
#### 10. Check application status
# GitLab QA
## Login
- Regular account login
- LDAP login
Use the [support document](https://docs.google.com/document/d/1cAHvbdFE6zR5WY-zhn3HsDcACssJE8Cav6WeYq3oCkM/edit#heading=h.2x3u50ukp87w) for the ldap settings.
## Forks
- fork group project
- push changes to fork
- submit merge request to origin project
- accept merge request
## Git
- add, remove ssh key
- git clone, git push over ssh
- git clone, git push over http (with both regular and ldap accounts)
## Project
- create project
- create project using import repo
- transfer project
- rename repo path
- add/remove project member
- remove project
- create git branch with UI
- create git tag with UI
## Web editor
- create, edit, remove file in web UI
## Group
- create group
- create project in group
- add/remove group member
- remove group
## Markdown
- Visit / clone [relative links repository](https://dev.gitlab.org/samples/relative-links/tree/master) and see if the links are linking to the correct documents in the repository
- Check if images are rendered in the md
- Click on a [directory link](https://dev.gitlab.org/samples/relative-links/tree/master/documents) and see if it correctly takes to the tree view
- Click on a [file link](https://dev.gitlab.org/samples/relative-links/blob/master/documents/0.md) and see if it correctly takes to the blob view
- Check if the links in the README when viewed as a [blob](https://dev.gitlab.org/samples/relative-links/blob/master/README.md) are correct
- Select the "markdown" branch and check if all links point to the files within the markdown branch
## Syntax highlighting
- Visit/clone [language highlight repository](https://dev.gitlab.org/samples/languages-highlight)
- Check for obvious errors in highlighting
## Upgrader
- Upgrade from the previous release
- Run the upgrader script in this release (it should not break)
## Rake tasks
- Check if rake gitlab:check is updated and works
- Check if rake gitlab:env:info is updated and works
# How to push GitLab CE master branch to all remotes.
The source code of GitLab is available on multiple servers (with GitLab.com as the canonical source).
Synchronization between the repo's is done by the lead developer if there is no rush.
This happens a few times per workday on average.
If somebody else with access to all repo's wants to do it the instructions are below.
This is just to distribute changes, not to make them.
## Add this to `.bashrc` or [your dotfiles](https://github.com/dosire/dotfiles/commit/52803ce3ac60d57632164b7713ff0041e86fa26c)
```bash
gpa ()
{
git push origin ${1:-master} && git push gh ${1:-master} && git push gl ${1:-master}
}
```
## Then add remotes to your local repo
```bash
cd my-gitlab-ce-repo
git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git
git remote add gh git@github.com:gitlabhq/gitlabhq.git
git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git
```
## Push to all remotes
```bash
gpa
```
# Yanking packages from packages.gitlab.com
In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the
procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg).
You need to have:
1. `package_cloud` gem installed (sudo gem install package_cloud)
1. Email and password for packages.gitlab.com
1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io)
Example of yanking a package:
```bash
package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm
```
If you are attempting this for the first time the output will look something like:
```bash
Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one.
Email:
marin@gitlab.com
Password:
Got your token. Writing a config file to /Users/marin/.packagecloud... success!
success!
Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done!
```
This diff is collapsed.
# Things to do when doing a patch release
NOTE: This is a guide for GitLab developers. If you are trying to install GitLab
see the latest stable [installation guide](install/installation.md) and if you
are trying to upgrade, see the [upgrade guides](update).
## When to do a patch release
Patch releases are done as-needed in order to fix regressions in the current
major release that cannot or should not wait until the next major release.
What's included and when to release is at the discretion of the release manager.
## Release Procedure
### Create a patch issue
Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with
the `release` label, and assign it to the milestone of the corresponding major
release.
Use the following template:
```
- Picked into respective `stable` branches:
- [ ] Merge `x-y-stable` into `x-y-stable-ee`
- [ ] release-tools: `x.y.z`
- omnibus-gitlab
- [ ] `x.y.z+ee.0`
- [ ] `x.y.z+ce.0`
- [ ] Deploy
- [ ] Add patch notice to [x.y regressions]()
- [ ] [Blog post]()
- [ ] [Tweet]()
- [ ] Add entry to version.gitlab.com
```
Update the issue with links to merge requests that need to be/have been picked
into the `stable` branches.
### Preparation
1. Verify that the issue can be reproduced
1. Note in the 'GitLab X.X regressions' that you will create a patch
1. Fix the issue on a feature branch, do this on the private GitLab development server
1. If it is a security issue, then assign it to the release manager and apply a 'security' label
1. Consider creating and testing workarounds
1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch
1. Make sure that the build has passed and all tests are passing
1. In a separate commit in the master branch update the CHANGELOG
1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X"
1. Merge CE stable branch into EE stable branch
### Bump version
Get release tools
```
git clone git@dev.gitlab.org:gitlab/release-tools.git
cd release-tools
```
Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now,
it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1.
Create release tag and push to remotes:
```
bundle exec rake release["x.x.x"]
```
## Release
1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
1. Apply the patch to GitLab.com and the private GitLab development server
1. Apply the patch to ci.gitLab.com and the private GitLab CI development server
1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md)
1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post
1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
1. Create the 'x.y.0' version on version.gitlab.com
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
1. Create a new patch release issue for the next potential release
# Things to do when doing an out-of-bound security release
NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update).
## When to do a security release
Do a security release when there is a critical issue that needs to be addresses before the next monthly release. Otherwise include it in the monthly release and note there was a security fix in the release announcement.
## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Release Procedure
1. Verify that the issue can be reproduced
1. Acknowledge the issue to the researcher that disclosed it
1. Inform the release manager that there needs to be a security release
1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server"
1. The MR with the security fix should get a 'security' label and be assigned to the release manager
1. Build the package for GitLab.com and do a deploy
1. Build the package for ci.gitLab.com and do a deploy
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
1. Create feature branches for the blog post on GitLab.com and link them from the code branch
1. Merge and publish the blog posts
1. Send tweets about the release from `@gitlabhq`
1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/)
1. Thank the security researcher in an email for their cooperation
1. Update the blog post and the CHANGELOG when we receive the CVE number
The timing of the code merge into master should be coordinated in advance.
After the merge we strive to publish the announcements within 60 minutes.
## Blog post template
XXX Security Advisory for GitLab
A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately. We [have|haven't|XXX|PICKSOMETHING|] heard of this vulnerability being actively exploited.
### Version affected
GitLab Community Edition XXX and lower
GitLab Enterprise Edition XXX and lower
### Fixed versions
GitLab Community Edition XXX and up
GitLab Enterprise Edition XXX and up
### Impact
On GitLab installations which use MySQL as their database backend it is possible for an attacker to assume the identity of any existing GitLab user in certain API calls. This attack can be performed by [unauthenticated|authenticated|XXX|PICKSOMETHING] users.
### Workarounds
If you are unable to upgrade you should apply the following patch and restart GitLab.
XXX
### Credit
We want to thank XXX of XXX for the responsible disclosure of this vulnerability.
## Email template
We just announced a security advisory for GitLab at XXX
Please contact us at support@gitlab.com if you have any questions.
## Tweet template
We just announced a security advisory for GitLab at XXX
...@@ -61,13 +61,13 @@ X-Gitlab-Event: Push Hook ...@@ -61,13 +61,13 @@ X-Gitlab-Event: Push Hook
"path_with_namespace":"mike/diaspora", "path_with_namespace":"mike/diaspora",
"default_branch":"master", "default_branch":"master",
"homepage":"http://example.com/mike/diaspora", "homepage":"http://example.com/mike/diaspora",
"url":"git@example.com:mike/diasporadiaspora.git", "url":"git@example.com:mike/diaspora.git",
"ssh_url":"git@example.com:mike/diaspora.git", "ssh_url":"git@example.com:mike/diaspora.git",
"http_url":"http://example.com/mike/diaspora.git" "http_url":"http://example.com/mike/diaspora.git"
}, },
"repository":{ "repository":{
"name": "Diaspora", "name": "Diaspora",
"url": "git@example.com:mike/diasporadiaspora.git", "url": "git@example.com:mike/diaspora.git",
"description": "", "description": "",
"homepage": "http://example.com/mike/diaspora", "homepage": "http://example.com/mike/diaspora",
"git_http_url":"http://example.com/mike/diaspora.git", "git_http_url":"http://example.com/mike/diaspora.git",
...@@ -116,7 +116,6 @@ Triggered when you create (or delete) tags to the repository. ...@@ -116,7 +116,6 @@ Triggered when you create (or delete) tags to the repository.
X-Gitlab-Event: Tag Push Hook X-Gitlab-Event: Tag Push Hook
``` ```
**Request body:** **Request body:**
```json ```json
...@@ -146,7 +145,7 @@ X-Gitlab-Event: Tag Push Hook ...@@ -146,7 +145,7 @@ X-Gitlab-Event: Tag Push Hook
"http_url":"http://example.com/jsmith/example.git" "http_url":"http://example.com/jsmith/example.git"
}, },
"repository":{ "repository":{
"name": "jsmith", "name": "Example",
"url": "ssh://git@example.com/jsmith/example.git", "url": "ssh://git@example.com/jsmith/example.git",
"description": "", "description": "",
"homepage": "http://example.com/jsmith/example", "homepage": "http://example.com/jsmith/example",
...@@ -481,7 +480,7 @@ X-Gitlab-Event: Note Hook ...@@ -481,7 +480,7 @@ X-Gitlab-Event: Note Hook
}, },
"repository":{ "repository":{
"name":"diaspora", "name":"diaspora",
"url":"git@example.com:mike/diasporadiaspora.git", "url":"git@example.com:mike/diaspora.git",
"description":"", "description":"",
"homepage":"http://example.com/mike/diaspora" "homepage":"http://example.com/mike/diaspora"
}, },
......
...@@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`: ...@@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`:
```ruby ```ruby
gitlab_rails['lfs_enabled'] = false gitlab_rails['lfs_enabled'] = false
# Optionally, change the storage path location. Defaults to
# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects" gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
``` ```
......
...@@ -42,10 +42,11 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps ...@@ -42,10 +42,11 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end end
step 'I click "All" link' do step 'I click "All" link' do
find(".js-author-search").click find('.js-author-search').click
find(".dropdown-menu-author li a", match: :first).click find('.dropdown-content a', match: :first).click
find(".js-assignee-search").click
find(".dropdown-menu-assignee li a", match: :first).click find('.js-assignee-search').click
find('.dropdown-content a', match: :first).click
end end
def should_see(issue) def should_see(issue)
......
...@@ -242,9 +242,9 @@ module Ci ...@@ -242,9 +242,9 @@ module Ci
stage_index = stages.index(job[:stage]) stage_index = stages.index(job[:stage])
job[:dependencies].each do |dependency| job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency] raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
unless stages.index(@jobs[dependency][:stage]) < stage_index unless stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end end
end end
......
...@@ -45,12 +45,12 @@ module Gitlab ...@@ -45,12 +45,12 @@ module Gitlab
note = create_note(reply) note = create_note(reply)
unless note.persisted? unless note.persisted?
message = "The comment could not be created for the following reasons:" msg = "The comment could not be created for the following reasons:"
note.errors.full_messages.each do |error| note.errors.full_messages.each do |error|
message << "\n\n- #{error}" msg << "\n\n- #{error}"
end end
raise InvalidNoteError, message raise InvalidNoteError, msg
end end
end end
...@@ -63,13 +63,13 @@ module Gitlab ...@@ -63,13 +63,13 @@ module Gitlab
end end
def reply_key def reply_key
reply_key = nil key = nil
message.to.each do |address| message.to.each do |address|
reply_key = Gitlab::IncomingEmail.key_from_address(address) key = Gitlab::IncomingEmail.key_from_address(address)
break if reply_key break if key
end end
reply_key key
end end
def sent_notification def sent_notification
......
...@@ -15,6 +15,25 @@ module Gitlab ...@@ -15,6 +15,25 @@ module Gitlab
# seconds then two overlapping operations may hold a lease for the same # seconds then two overlapping operations may hold a lease for the same
# key at the same time. # key at the same time.
# #
# This class has no 'cancel' method. I originally decided against adding
# it because it would add complexity and a false sense of security. The
# complexity: instead of setting '1' we would have to set a UUID, and to
# delete it we would have to execute Lua on the Redis server to only
# delete the key if the value was our own UUID. Otherwise there is a
# chance that when you intend to cancel your lease you actually delete
# someone else's. The false sense of security: you cannot design your
# system to rely too much on the lease being cancelled after use because
# the calling (Ruby) process may crash or be killed. You _cannot_ count
# on begin/ensure blocks to cancel a lease, because the 'ensure' does
# not always run. Think of 'kill -9' from the Unicorn master for
# instance.
#
# If you find that leases are getting in your way, ask yourself: would
# it be enough to lower the lease timeout? Another thing that might be
# appropriate is to only use a lease for bulk/automated operations, and
# to ignore the lease when you get a single 'manual' user request (a
# button click).
#
class ExclusiveLease class ExclusiveLease
def initialize(key, timeout:) def initialize(key, timeout:)
@key, @timeout = key, timeout @key, @timeout = key, timeout
...@@ -27,6 +46,8 @@ module Gitlab ...@@ -27,6 +46,8 @@ module Gitlab
!!redis.set(redis_key, '1', nx: true, ex: @timeout) !!redis.set(redis_key, '1', nx: true, ex: @timeout)
end end
# No #cancel method. See comments above!
private private
def redis def redis
......
require 'rails_helper' require 'rails_helper'
describe GroupsController do describe GroupsController do
describe 'GET index' do let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
describe 'GET #index' do
context 'as a user' do context 'as a user' do
it 'redirects to Groups Dashboard' do it 'redirects to Groups Dashboard' do
sign_in(create(:user)) sign_in(user)
get :index get :index
...@@ -20,4 +25,54 @@ describe GroupsController do ...@@ -20,4 +25,54 @@ describe GroupsController do
end end
end end
end end
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
before do
create_list(:upvote_note, 3, project: project, noteable: issue_2)
create_list(:upvote_note, 2, project: project, noteable: issue_1)
create_list(:downvote_note, 2, project: project, noteable: issue_2)
sign_in(user)
end
context 'sorting by votes' do
it 'sorts most popular issues' do
get :issues, id: group.to_param, sort: 'upvotes_desc'
expect(assigns(:issues)).to eq [issue_2, issue_1]
end
it 'sorts least popular issues' do
get :issues, id: group.to_param, sort: 'downvotes_desc'
expect(assigns(:issues)).to eq [issue_2, issue_1]
end
end
end
describe 'GET #merge_requests' do
let(:merge_request_1) { create(:merge_request, source_project: project) }
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do
create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
sign_in(user)
end
context 'sorting by votes' do
it 'sorts most popular merge requests' do
get :merge_requests, id: group.to_param, sort: 'upvotes_desc'
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
it 'sorts least popular merge requests' do
get :merge_requests, id: group.to_param, sort: 'downvotes_desc'
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
end
end
end end
...@@ -43,6 +43,28 @@ describe RootController do ...@@ -43,6 +43,28 @@ describe RootController do
end end
end end
context 'who has customized their dashboard setting for groups' do
before do
user.update_attribute(:dashboard, 'groups')
end
it 'redirects to their group list' do
get :index
expect(response).to redirect_to dashboard_groups_path
end
end
context 'who has customized their dashboard setting for todos' do
before do
user.update_attribute(:dashboard, 'todos')
end
it 'redirects to their todo list' do
get :index
expect(response).to redirect_to dashboard_todos_path
end
end
context 'who uses the default dashboard setting' do context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do it 'renders the default dashboard' do
get :index get :index
......
require 'spec_helper'
feature 'Dashboard > Milestones', feature: true do
describe 'as anonymous user' do
before do
visit dashboard_milestones_path
end
it 'is redirected to sign-in page' do
expect(current_path).to eq new_user_session_path
end
end
describe 'as logged-in user' do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
before do
project.team << [user, :master]
login_with(user)
visit dashboard_milestones_path
end
it 'sees milestones' do
expect(current_path).to eq dashboard_milestones_path
expect(page).to have_content(milestone.title)
end
end
end
require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
project.team << [user, :master]
login_as(user)
end
context 'status', js: true do
it 'should be set to closed' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Closed').click
click_update_issues_button
expect(page).to have_selector('.issue', count: 0)
end
it 'should be set to open' do
create_closed
visit namespace_project_issues_path(project.namespace, project)
find('.issues-state-filters a', text: 'Closed').click
find('#check_all_issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Open').click
click_update_issues_button
expect(page).to have_selector('.issue', count: 0)
end
end
context 'assignee', js: true do
it 'should update to current user' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
find('.js-update-assignee').click
find('.dropdown-menu-user-link', text: user.username).click
click_update_issues_button
page.within('.issue .controls') do
expect(find('.author_link')["data-original-title"]).to have_content(user.name)
end
end
it 'should update to unassigned' do
create_assigned
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
find('.js-update-assignee').click
click_link 'Unassigned'
click_update_issues_button
within first('.issue .controls') do
expect(page).to have_no_selector('.author_link')
end
end
end
context 'milestone', js: true do
let(:milestone) { create(:milestone, project: project) }
it 'should update milestone' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: milestone.title).click
click_update_issues_button
expect(find('.issue')).to have_content milestone.title
end
it 'should set to no milestone' do
create_with_milestone
visit namespace_project_issues_path(project.namespace, project)
expect(first('.issue')).to have_content milestone.title
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button
expect(first('.issue')).to_not have_content milestone.title
end
end
def create_closed
create(:issue, project: project, state: :closed)
end
def create_assigned
create(:issue, project: project, assignee: user)
end
def create_with_milestone
create(:issue, project: project, milestone: milestone)
end
def click_update_issues_button
find('.update_selected_issues').click
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment