BigW Consortium Gitlab

Commit f4d478a6 by Jacob Vosmaer

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into expiring-lock

parents 3729c082 3d4063ef
...@@ -19,11 +19,11 @@ before_script: ...@@ -19,11 +19,11 @@ before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
- ruby -v - ruby -v
- which ruby - which ruby
- gem install bundler --no-ri --no-rdoc - retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log - touch log/application.log
- touch log/test.log - touch log/test.log
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
stages: stages:
......
...@@ -14,16 +14,22 @@ v 8.6.0 (unreleased) ...@@ -14,16 +14,22 @@ v 8.6.0 (unreleased)
- Strip leading and trailing spaces in URL validator (evuez) - Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API - Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Add support for cross-project label references - Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - Allow search for logged out users
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view - Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip) - Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views - Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho) - Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages - Add ability to show archived projects on dashboard, explore and group pages
v 8.5.5
- Ensure removing a project removes associated Todo entries.
- Prevent a 500 error in Todos when author was removed.
v 8.5.4 v 8.5.4
- Do not cache requests for badges (including builds badge) - Do not cache requests for badges (including builds badge)
......
...@@ -275,7 +275,7 @@ group :development, :test do ...@@ -275,7 +275,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0' gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.8.1' gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0' gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0' gem 'teaspoon-jasmine', '~> 2.2.0'
......
...@@ -552,7 +552,7 @@ GEM ...@@ -552,7 +552,7 @@ GEM
parser (2.2.3.0) parser (2.2.3.0)
ast (>= 1.1, < 3.0) ast (>= 1.1, < 3.0)
pg (0.18.4) pg (0.18.4)
poltergeist (1.8.1) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
cliver (~> 0.3.1) cliver (~> 0.3.1)
multi_json (~> 1.0) multi_json (~> 1.0)
...@@ -982,7 +982,7 @@ DEPENDENCIES ...@@ -982,7 +982,7 @@ DEPENDENCIES
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
paranoia (~> 2.0) paranoia (~> 2.0)
pg (~> 0.18.2) pg (~> 0.18.2)
poltergeist (~> 1.8.1) poltergeist (~> 1.9.0)
pry-rails pry-rails
quiet_assets (~> 1.0.2) quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1) rack-attack (~> 4.3.1)
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
class @MarkdownPreview class @MarkdownPreview
# Minimum number of users referenced before triggering a warning # Minimum number of users referenced before triggering a warning
referenceThreshold: 10 referenceThreshold: 10
ajaxCache: {}
showPreview: (form) -> showPreview: (form) ->
preview = form.find('.js-md-preview') preview = form.find('.js-md-preview')
...@@ -24,12 +25,16 @@ class @MarkdownPreview ...@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) -> renderMarkdown: (text, success) ->
return unless window.markdown_preview_path return unless window.markdown_preview_path
return success(@ajaxCache.response) if text == @ajaxCache.text
$.ajax $.ajax
type: 'POST' type: 'POST'
url: window.markdown_preview_path url: window.markdown_preview_path
data: { text: text } data: { text: text }
dataType: 'json' dataType: 'json'
success: success success: (response) =>
@ajaxCache = text: text, response: response
success(response)
hideReferencedUsers: (form) -> hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users') referencedUsers = form.find('.referenced-users')
...@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview() ...@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button' previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button' writeButtonSelector = '.js-md-write-button'
lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = -> $.fn.setupMarkdownPreview = ->
$form = $(this) $form = $(this)
...@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = -> ...@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form) form_textarea.on 'blur', -> markdownPreview.showPreview($form)
$(document).on 'click', previewButtonSelector, (e) -> $(document).on 'markdown-preview:show', (e, $form) ->
e.preventDefault() return unless $form
$form = $(this).closest('form') lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs # toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active') $form.find(writeButtonSelector).parent().removeClass('active')
...@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) -> ...@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form) markdownPreview.showPreview($form)
$(document).on 'click', writeButtonSelector, (e) -> $(document).on 'markdown-preview:hide', (e, $form) ->
e.preventDefault() return unless $form
$form = $(this).closest('form') lastTextareaPreviewed = null
# toggle tabs # toggle tabs
$form.find(writeButtonSelector).parent().addClass('active') $form.find(writeButtonSelector).parent().addClass('active')
...@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) -> ...@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content # toggle content
$form.find('.md-write-holder').show() $form.find('.md-write-holder').show()
$form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide() $form.find('.md-preview-holder').hide()
$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
$target = $(keyboardEvent.target)
if $target.is('textarea.markdown-area')
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
keyboardEvent.preventDefault()
else if lastTextareaPreviewed
$target = lastTextareaPreviewed
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
keyboardEvent.preventDefault()
$(document).on 'click', previewButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:show', [$form])
$(document).on 'click', writeButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:hide', [$form])
...@@ -31,7 +31,7 @@ class @Notes ...@@ -31,7 +31,7 @@ class @Notes
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# change note in UI after update # change note in UI after update
$(document).on "ajax:success", "form.edit_note", @updateNote $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link # Edit note link
$(document).on "click", ".js-note-edit", @showEditForm $(document).on "click", ".js-note-edit", @showEditForm
...@@ -72,7 +72,7 @@ class @Notes ...@@ -72,7 +72,7 @@ class @Notes
cleanBinding: -> cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form" $(document).off "ajax:success", ".js-discussion-note-form"
$(document).off "ajax:success", "form.edit_note" $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit" $(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel" $(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-delete"
...@@ -347,22 +347,26 @@ class @Notes ...@@ -347,22 +347,26 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").hide() note.find(".note-body > .note-text").hide()
note.find(".note-header").hide() note.find(".note-header").hide()
base_form = note.find(".note-edit-form") form = note.find(".note-edit-form")
form = base_form.clone().insertAfter(base_form) isNewForm = form.is(':not(.gfm-form)')
form.addClass('current-note-edit-form gfm-form') if isNewForm
form.find('.div-dropzone').remove() form.addClass('gfm-form')
form.addClass('current-note-edit-form')
form.show()
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
# Setup markdown form # Setup markdown form
GitLab.GfmAutoComplete.setup() if isNewForm
new DropzoneInput(form) GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
textarea = form.find("textarea") textarea = form.find("textarea")
textarea.focus() textarea.focus()
autosize(textarea)
if isNewForm
autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we # The textarea has the correct value, Chrome just won't show it unless we
...@@ -371,7 +375,8 @@ class @Notes ...@@ -371,7 +375,8 @@ class @Notes
textarea.val "" textarea.val ""
textarea.val value textarea.val value
disableButtonIfEmptyField textarea, form.find(".js-comment-button") if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
...@@ -383,7 +388,9 @@ class @Notes ...@@ -383,7 +388,9 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").show() note.find(".note-body > .note-text").show()
note.find(".note-header").show() note.find(".note-header").show()
note.find(".current-note-edit-form").remove() note.find(".current-note-edit-form")
.removeClass("current-note-edit-form")
.hide()
### ###
Called in response to deleting a note of any kind. Called in response to deleting a note of any kind.
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
init: -> init: ->
$(".projects-list-filter").off('keyup') $(".projects-list-filter").off('keyup')
this.initSearch() this.initSearch()
this.initPagination()
initSearch: -> initSearch: ->
@timer = null @timer = null
...@@ -29,3 +30,8 @@ ...@@ -29,3 +30,8 @@
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json" dataType: "json"
initPagination: ->
$('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
$('.projects-list-holder').replaceWith(data.html)
)
...@@ -4,11 +4,15 @@ class @Shortcuts ...@@ -4,11 +4,15 @@ class @Shortcuts
Mousetrap.reset() Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp) Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) => selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp) Shortcuts.showHelp(e, @enabledHelp)
toggleMarkdownPreview: (e) =>
$(document).triggerHandler('markdown-preview:toggle', [e])
@showHelp: (e, location) -> @showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0 if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show') $('#modal-shortcuts').modal('show')
...@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) -> ...@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove() $(@).remove()
$('.hidden-shortcut').show() $('.hidden-shortcut').show()
e.preventDefault() e.preventDefault()
Mousetrap.stopCallback = (->
defaultStopCallback = Mousetrap.stopCallback
return (e, element, combo) ->
# allowed shortcuts if textarea, input, contenteditable are focused
if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
return false
else
return defaultStopCallback.apply(@, arguments)
)()
...@@ -17,6 +17,47 @@ ...@@ -17,6 +17,47 @@
.dropdown-menu { .dropdown-menu {
display: block; display: block;
} }
.dropdown-menu-toggle {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu-toggle {
position: relative;
min-width: 160px;
padding: 5px 20px 5px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border-radius: 2px;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.fa {
position: absolute;
top: 50%;
right: 6px;
margin-top: -4px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
} }
.dropdown-menu { .dropdown-menu {
...@@ -36,6 +77,21 @@ ...@@ -36,6 +77,21 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
.dropdown-content {
display: none;
}
.dropdown-loading {
display: block;
}
}
ul {
margin: 0;
padding: 0;
}
li { li {
text-align: left; text-align: left;
list-style: none; list-style: none;
...@@ -68,6 +124,57 @@ ...@@ -68,6 +124,57 @@
} }
} }
.dropdown-menu-paging {
.dropdown-page-two {
display: none;
}
&.is-page-two {
.dropdown-page-one {
display: none;
}
.dropdown-page-two {
display: block;
}
}
}
.dropdown-menu-user {
.avatar {
float: left;
width: 30px;
height: 30px;
margin: 0 10px 0 0;
}
}
.dropdown-menu-user-link {
padding-top: 7px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
margin-bottom: 2px;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-menu-user-username {
display: block;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-select {
width: 280px;
}
.dropdown-menu-align-right { .dropdown-menu-align-right {
left: auto; left: auto;
right: 0; right: 0;
...@@ -101,3 +208,112 @@ ...@@ -101,3 +208,112 @@
font-size: 13px; font-size: 13px;
line-height: 22px; line-height: 22px;
} }
.dropdown-title {
position: relative;
margin-bottom: 10px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600;
line-height: 1;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: 1px solid $dropdown-divider-color;
overflow: hidden;
}
.dropdown-title-button {
position: absolute;
top: -1px;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
border: 0;
background: none;
&:hover {
color: darken($dropdown-title-btn-color, 15%);
}
}
.dropdown-menu-close {
right: 0;
}
.dropdown-menu-back {
left: 0;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
.fa {
position: absolute;
top: 10px;
right: 10px;
color: #C7C7C7;
font-size: 12px;
pointer-events: none;
}
}
.dropdown-input-field {
width: 100%;
padding: 0 7px;
color: $dropdown-input-color;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border-radius: 2px;
outline: 0;
&:focus {
color: $dropdown-link-color;
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa {
color: $dropdown-link-color;
}
}
&:hover {
+ .fa {
color: $dropdown-link-color;
}
}
}
.dropdown-content {
max-height: 200px;
overflow-y: scroll;
}
.dropdown-footer {
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
border-top: 1px solid $dropdown-divider-color;
}
.dropdown-loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
z-index: 9;
background-color: $dropdown-loading-bg;
font-size: 28px;
.fa {
position: absolute;
top: 50%;
left: 50%;
margin-top: -14px;
margin-left: -14px;
}
}
...@@ -149,13 +149,13 @@ ...@@ -149,13 +149,13 @@
} }
&:hover > a.anchor { &:hover > a.anchor {
$size: 16px; $size: 14px;
position: absolute; position: absolute;
right: 100%; right: 100%;
top: 50%; top: 50%;
margin-top: -$size/2; margin-top: -11px;
margin-right: 0px; margin-right: 0;
padding-right: 20px; padding-right: 15px;
display: inline-block; display: inline-block;
width: $size; width: $size;
height: $size; height: $size;
......
...@@ -138,3 +138,15 @@ $dropdown-shadow-color: rgba(#000, .1); ...@@ -138,3 +138,15 @@ $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-caret-color: #54565B; $dropdown-caret-color: #54565B;
$dropdown-title-btn-color: #BFBFBF;
$dropdown-input-color: #C7C7C7;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
$dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
...@@ -26,5 +26,13 @@ ...@@ -26,5 +26,13 @@
margin-right: 10px; margin-right: 10px;
font-size: $gl-font-size; font-size: $gl-font-size;
border: 1px solid; border: 1px solid;
line-height: 40px; line-height: 32px;
}
.markdown-snippet-copy {
position: fixed;
top: -10px;
left: -10px;
max-height: 0;
max-width: 0;
} }
...@@ -72,7 +72,7 @@ module ApplicationHelper ...@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user = user_or_email user = user_or_email
else else
user = User.find_by(email: user_or_email.downcase) user = User.find_by(email: user_or_email.try(:downcase))
end end
if user if user
......
...@@ -211,4 +211,15 @@ module CommitsHelper ...@@ -211,4 +211,15 @@ module CommitsHelper
def clean(string) def clean(string)
Sanitize.clean(string, remove_contents: true) Sanitize.clean(string, remove_contents: true)
end end
def limited_commits(commits)
if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
[
commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
]
else
[commits, 0]
end
end
end end
...@@ -9,6 +9,7 @@ class Ability ...@@ -9,6 +9,7 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject) when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject) when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject) when Issue then issue_abilities(user, subject)
when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject) when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject)
...@@ -424,6 +425,10 @@ class Ability ...@@ -424,6 +425,10 @@ class Ability
end end
end end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private private
def named_abilities(name) def named_abilities(name)
......
...@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
# Prevent store of diff if commits amount more then 500 # Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500 COMMITS_SAFE_SIZE = 100
belongs_to :merge_request belongs_to :merge_request
......
...@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base ...@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects has_many :lfs_objects, through: :lfs_objects_projects
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
...@@ -527,11 +528,11 @@ class Project < ActiveRecord::Base ...@@ -527,11 +528,11 @@ class Project < ActiveRecord::Base
end end
def ci_services def ci_services
services.select { |service| service.category == :ci } services.where(category: :ci)
end end
def ci_service def ci_service
@ci_service ||= ci_services.find(&:activated?) @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end end
def jira_tracker? def jira_tracker?
......
...@@ -4,7 +4,10 @@ ...@@ -4,7 +4,10 @@
.todo-title .todo-title
%span.author-name %span.author-name
= link_to_author todo - if todo.author
= link_to_author(todo)
- else
(removed)
%span.todo-label %span.todo-label
= todo_action_name(todo) = todo_action_name(todo)
= todo_target_link(todo) = todo_target_link(todo)
......
...@@ -22,6 +22,14 @@ ...@@ -22,6 +22,14 @@
%td.shortcut %td.shortcut
.key ? .key ?
%td Show this dialog %td Show this dialog
%tr
%td.shortcut
- if browser.mac?
.key &#8984; shift p
- else
.key ctrl shift p
%td Toggle Markdown preview
%tbody %tbody
%tr %tr
%th %th
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
%li %li
= link_to 'Buttons', '#buttons' = link_to 'Buttons', '#buttons'
%li %li
= link_to 'Dropdowns', '#dropdowns'
%li
= link_to 'Panels', '#panels' = link_to 'Panels', '#panels'
%li %li
= link_to 'Alerts', '#alerts' = link_to 'Alerts', '#alerts'
...@@ -180,9 +182,9 @@ ...@@ -180,9 +182,9 @@
.nav-controls .nav-controls
= text_field_tag 'sample', nil, class: 'form-control' = text_field_tag 'sample', nil, class: 'form-control'
.dropdown .dropdown
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span Sort by name %span Sort by name
%b.caret = icon('chevron-down')
%ul.dropdown-menu %ul.dropdown-menu
%li %li
%a Sort by date %a Sort by date
...@@ -212,6 +214,182 @@ ...@@ -212,6 +214,182 @@
%button.btn.btn-danger{:type => "button"} Danger %button.btn.btn-danger{:type => "button"} Danger
%button.btn.btn-link{:type => "button"} Link %button.btn.btn-link{:type => "button"} Link
%h2#dropdowns Dropdowns
.example
.clearfix
.dropdown.inline.pull-left
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a{href: "#"}
Dropdown Option
.dropdown.inline.pull-right
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
%a{href: "#"}
Dropdown Option
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-selectable
%li
%a.is-active{href: "#"}
Dropdown Option
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.is-active{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li.divider
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
.dropdown-footer
%strong Tip:
If an author is not a member of this project, you can still filter by his name while using the search field.
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown loading
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
.dropdown-title
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.is-active{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li.divider
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
%li
%a{href: "#"}
Dropdown Option
.dropdown-footer
%strong Tip:
If an author is not a member of this project, you can still filter by his name while using the search field.
.dropdown-loading
= icon('spinner spin')
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown user
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
.dropdown-title
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30)
%strong.dropdown-menu-user-full-name
= current_user.name
.dropdown-menu-user-username
= current_user.to_reference
.example
%div
.dropdown.inline
%button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Dropdown page 2
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
.dropdown-page-one
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
= icon('arrow-left')
%span Dropdown Title
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Filter results"}
= icon('search')
.dropdown-content
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30)
%strong.dropdown-menu-user-full-name
= current_user.name
.dropdown-menu-user-username
= current_user.to_reference
.dropdown-page-two
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
= icon('arrow-left')
%span Create label
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.dropdown-input
%input.dropdown-input-field{type: "search", placeholder: "Name new label"}
.dropdown-content
%button.btn.btn-primary
Create
%h2#panels Panels %h2#panels Panels
.row .row
......
- unless @project.empty_repo? - unless @project.empty_repo?
- if can? current_user, :download_code, @project - if can? current_user, :download_code, @project
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
= icon('download') = icon('download')
- commits, hidden = limited_commits(@commits)
- commits = Commit.decorate(commits, @project)
%div.panel.panel-default %div.panel.panel-default
.panel-heading .panel-heading
Commits (#{@commits.count}) Commits (#{@commits.count})
- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - if hidden > 0
%ul.well-list %ul.well-list
- Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).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
other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else - else
%ul.well-list= render Commit.decorate(@commits, @project), project: @project %ul.well-list= render commits, project: @project
- unless defined?(project) - unless defined?(project)
- project = @project - project = @project
- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| - commits, hidden = limited_commits(@commits)
- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
.row.commits-row .row.commits-row
.col-md-2.hidden-xs.hidden-sm .col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date %h5.commits-row-date
...@@ -13,3 +15,7 @@ ...@@ -13,3 +15,7 @@
%ul.bordered-list %ul.bordered-list
= render commits, project: project = render commits, project: project
%hr.lists-separator %hr.lists-separator
- if hidden > 0
.alert.alert-warning
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
.note-edit-form .note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, class: 'js-quick-submit' do |f| = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
= note_target_fields(note) = note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
......
- unless @snippet.content.empty? - unless @snippet.content.empty?
- if markup?(@snippet.file_name) - if markup?(@snippet.file_name)
%textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
= @snippet.data
.file-content.wiki .file-content.wiki
= render_markup(@snippet.file_name, @snippet.data) = render_markup(@snippet.file_name, @snippet.data)
- else - else
......
class FixTodos < ActiveRecord::Migration
def up
execute <<-SQL
DELETE FROM todos
WHERE todos.target_type IN ('Commit', 'ProjectSnippet')
OR NOT EXISTS (
SELECT *
FROM projects
WHERE projects.id = todos.project_id
)
SQL
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160305220806) do ActiveRecord::Schema.define(version: 20160309140734) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
# This task will generate a standard and Retina sprite of all of the current
# Gemojione Emojis, with the accompanying SCSS map.
#
# It will not appear in `rake -T` output, and the dependent gems are not
# included in the Gemfile by default, because this task will only be needed
# occasionally, such as when new Emojis are added to Gemojione.
begin
require 'sprite_factory'
require 'rmagick'
rescue LoadError
# noop
end
namespace :gemojione do
task sprite: :environment do
check_requirements!
SIZE = 20
RETINA = SIZE * 2
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), SIZE)
end
end
style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
SpriteFactory.run!(File.join(tmpdir, 'images'), {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
style: :scss,
nocomments: true,
pngcrush: true,
layout: :packed
})
# SpriteFactory's SCSS is a bit too verbose for our purposes here, so
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
f.puts
f.puts <<-CSS.strip_heredoc
.emoji-icon {
background-image: image-url('emoji.png');
background-repeat: no-repeat;
height: #{SIZE}px;
width: #{SIZE}px;
@media only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (min--moz-device-pixel-ratio: 2),
only screen and (-o-min-device-pixel-ratio: 2/1),
only screen and (min-device-pixel-ratio: 2),
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
background-image: image-url('emoji@2x.png');
background-size: 840px 820px;
}
}
CSS
end
end
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), RETINA)
end
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.run!(File.join(tmpdir, 'images'), {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
pngcrush: true,
layout: :packed
})
end
end
def check_requirements!
return if defined?(SpriteFactory) && defined?(Magick)
puts <<-MSG.strip_heredoc
This task is disabled by default and should only be run when the Gemojione
gem is updated with new Emojis.
To enable this task, *temporarily* add the following lines to Gemfile and
re-bundle:
gem 'sprite-factory'
gem 'rmagick'
MSG
exit 1
end
def resize!(image_path, size)
# Resize the image in-place, save it, and free the object
image = Magick::Image.read(image_path).first
image.resize!(size, size)
image.write(image_path) { self.quality = 100 }
image.destroy!
end
end
#!/bin/bash #!/bin/bash
retry() {
for i in $(seq 1 3); do
if eval "$@"; then
return 0
fi
sleep 3s
echo "Retrying..."
done
return 1
}
if [ -f /.dockerinit ]; then if [ -f /.dockerinit ]; then
mkdir -p vendor mkdir -p vendor
...@@ -12,17 +23,8 @@ if [ -f /.dockerinit ]; then ...@@ -12,17 +23,8 @@ if [ -f /.dockerinit ]; then
popd popd
# Try to install packages # Try to install packages
for i in $(seq 1 3); do retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
apt-get update -yqqq || true libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
if apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip; then
break
fi
sleep 3s
echo "Retrying package installation..."
done
cp config/database.yml.mysql config/database.yml cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml
......
...@@ -105,6 +105,31 @@ describe Issue, models: true do ...@@ -105,6 +105,31 @@ describe Issue, models: true do
end end
end end
describe '#referenced_merge_requests' do
it 'returns the referenced merge requests' do
project = create(:project, :public)
mr1 = create(:merge_request,
source_project: project,
source_branch: 'master',
target_branch: 'feature')
mr2 = create(:merge_request,
source_project: project,
source_branch: 'feature',
target_branch: 'master')
issue = create(:issue, description: mr1.to_reference, project: project)
create(:note_on_issue,
noteable: issue,
note: mr2.to_reference,
project_id: project.id)
expect(issue.referenced_merge_requests).to eq([mr1, mr2])
end
end
it_behaves_like 'an editable mentionable' do it_behaves_like 'an editable mentionable' do
subject { create(:issue) } subject { create(:issue) }
......
...@@ -68,6 +68,7 @@ describe Project, models: true do ...@@ -68,6 +68,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) } it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
end end
describe 'modules' do describe 'modules' do
......
...@@ -271,22 +271,24 @@ describe GitPushService, services: true do ...@@ -271,22 +271,24 @@ describe GitPushService, services: true do
allow(project.repository).to receive(:commits_between). allow(project.repository).to receive(:commits_between).
and_return([closing_commit]) and_return([closing_commit])
project.team << [commit_author, :master]
end end
context "to default branches" do context "to default branches" do
it "closes issues" do it "closes issues" do
execute_service(project, user, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(Issue.find(issue.id)).to be_closed expect(Issue.find(issue.id)).to be_closed
end end
it "adds a note indicating that the issue is now closed" do it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
execute_service(project, user, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
end end
it "doesn't create additional cross-reference notes" do it "doesn't create additional cross-reference notes" do
expect(SystemNoteService).not_to receive(:cross_reference) expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, user, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
end end
it "doesn't close issues when external issue tracker is in use" do it "doesn't close issues when external issue tracker is in use" do
...@@ -294,7 +296,7 @@ describe GitPushService, services: true do ...@@ -294,7 +296,7 @@ describe GitPushService, services: true do
# The push still shouldn't create cross-reference notes. # The push still shouldn't create cross-reference notes.
expect do expect do
execute_service(project, user, @oldrev, @newrev, 'refs/heads/hurf' ) execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' )
end.not_to change { Note.where(project_id: project.id, system: true).count } end.not_to change { Note.where(project_id: project.id, system: true).count }
end end
end end
...@@ -316,7 +318,6 @@ describe GitPushService, services: true do ...@@ -316,7 +318,6 @@ describe GitPushService, services: true do
end end
end end
# EE-only tests
context "for jira issue tracker" do context "for jira issue tracker" do
include JiraServiceHelper include JiraServiceHelper
...@@ -366,7 +367,7 @@ describe GitPushService, services: true do ...@@ -366,7 +367,7 @@ describe GitPushService, services: true do
} }
}.to_json }.to_json
execute_service(project, user, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_transition_url).with( expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
body: transition_body body: transition_body
).once ).once
...@@ -377,7 +378,7 @@ describe GitPushService, services: true do ...@@ -377,7 +378,7 @@ describe GitPushService, services: true do
body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json }.to_json
execute_service(project, user, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url).with( expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: comment_body body: comment_body
).once ).once
......
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