Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into git-http-controller
This commit is contained in:
commit
2a9a9e147b
241 changed files with 6479 additions and 1052 deletions
31
CHANGELOG
31
CHANGELOG
|
@ -1,36 +1,57 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.7.0 (unreleased)
|
||||
- The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse)
|
||||
- Fix revoking of authorized OAuth applications (Connor Shea)
|
||||
- All service classes (those residing in app/services) are now instrumented (Yorick Peterse)
|
||||
- Developers can now add custom tags to transactions (Yorick Peterse)
|
||||
- Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse)
|
||||
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
|
||||
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
|
||||
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
|
||||
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
|
||||
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
|
||||
- Add setting for customizing the list of trusted proxies !3524
|
||||
- Allow projects to be transfered to a lower visibility level group
|
||||
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
|
||||
- Improved Markdown rendering performance !3389 (Yorick Peterse)
|
||||
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
|
||||
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
|
||||
- Expose project badges in project settings
|
||||
- Make /profile/keys/new redirect to /profile/keys for back-compat. !3717
|
||||
- Preserve time notes/comments have been updated at when moving issue
|
||||
- Make HTTP(s) label consistent on clone bar (Stan Hu)
|
||||
- Expose label description in API (Mariusz Jachimowicz)
|
||||
- Allow back dating on issues when created through the API
|
||||
- API: Ability to update a group (Robert Schilling)
|
||||
- API: Ability to move issues (Robert Schilling)
|
||||
- Fix Error 500 after renaming a project path (Stan Hu)
|
||||
- Fix a bug whith trailing slash in teamcity_url (Charles May)
|
||||
- Allow back dating on issues when created or updated through the API
|
||||
- Allow back dating on issue notes when created through the API
|
||||
- Fix avatar stretching by providing a cropping feature
|
||||
- API: Expose `subscribed` for issues and merge requests (Robert Schilling)
|
||||
- Allow SAML to handle external users based on user's information !3530
|
||||
- Allow Omniauth providers to be marked as `external` !3657
|
||||
- Add endpoints to archive or unarchive a project !3372
|
||||
- Fix a bug whith trailing slash in bamboo_url
|
||||
- Add links to CI setup documentation from project settings and builds pages
|
||||
- Handle nil descriptions in Slack issue messages (Stan Hu)
|
||||
- Add automated repository integrity checks
|
||||
- API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling)
|
||||
- API: Ability to star and unstar a project (Robert Schilling)
|
||||
- Add default scope to projects to exclude projects pending deletion
|
||||
- Allow to close merge requests which source projects(forks) are deleted.
|
||||
- Ensure empty recipients are rejected in BuildsEmailService
|
||||
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
|
||||
- API: Fix milestone filtering by `iid` (Robert Schilling)
|
||||
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
|
||||
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
|
||||
- Better errors handling when creating milestones inside groups
|
||||
- Fix high CPU usage when PostReceive receives refs/merge-requests/<id>
|
||||
- Hide `Create a group` help block when creating a new project in a group
|
||||
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
|
||||
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
|
||||
- Decouple membership and notifications
|
||||
- Fix creation of merge requests for orphaned branches (Stan Hu)
|
||||
- API: Ability to retrieve a single tag (Robert Schilling)
|
||||
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
|
||||
|
@ -38,13 +59,21 @@ v 8.7.0 (unreleased)
|
|||
- Fix admin/projects when using visibility levels on search (PotHix)
|
||||
- Build status notifications
|
||||
- API: Expose user location (Robert Schilling)
|
||||
- API: Do not leak group existence via return code (Robert Schilling)
|
||||
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
|
||||
- Update number of Todos in the sidebar when it's marked as "Done". !3600
|
||||
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
|
||||
- API: User can leave a project through the API when not master or owner. !3613
|
||||
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
|
||||
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
|
||||
- Improved markdown forms
|
||||
- Diffs load at the correct point when linking from from number
|
||||
- Selected diff rows highlight
|
||||
- Fix emoji catgories in the emoji picker
|
||||
|
||||
v 8.6.6
|
||||
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
|
||||
- Project switcher uses new dropdown styling
|
||||
|
||||
v 8.6.5
|
||||
- Fix importing from GitHub Enterprise. !3529
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -285,9 +285,9 @@ group :development, :test do
|
|||
gem 'teaspoon', '~> 1.1.0'
|
||||
gem 'teaspoon-jasmine', '~> 2.2.0'
|
||||
|
||||
gem 'spring', '~> 1.6.4'
|
||||
gem 'spring', '~> 1.7.0'
|
||||
gem 'spring-commands-rspec', '~> 1.0.4'
|
||||
gem 'spring-commands-spinach', '~> 1.0.0'
|
||||
gem 'spring-commands-spinach', '~> 1.1.0'
|
||||
gem 'spring-commands-teaspoon', '~> 0.0.2'
|
||||
|
||||
gem 'rubocop', '~> 0.38.0', require: false
|
||||
|
|
|
@ -769,10 +769,10 @@ GEM
|
|||
spinach (>= 0.4)
|
||||
spinach-rerun-reporter (0.0.2)
|
||||
spinach (~> 0.8)
|
||||
spring (1.6.4)
|
||||
spring (1.7.1)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
spring-commands-spinach (1.0.0)
|
||||
spring-commands-spinach (1.1.0)
|
||||
spring (>= 0.9.1)
|
||||
spring-commands-teaspoon (0.0.2)
|
||||
spring (>= 0.9.1)
|
||||
|
@ -1030,9 +1030,9 @@ DEPENDENCIES
|
|||
slack-notifier (~> 1.2.0)
|
||||
spinach-rails (~> 0.2.1)
|
||||
spinach-rerun-reporter (~> 0.0.2)
|
||||
spring (~> 1.6.4)
|
||||
spring (~> 1.7.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
spring-commands-spinach (~> 1.0.0)
|
||||
spring-commands-spinach (~> 1.1.0)
|
||||
spring-commands-teaspoon (~> 0.0.2)
|
||||
sprockets (~> 3.6.0)
|
||||
state_machines-activerecord (~> 0.3.0)
|
||||
|
|
|
@ -22,7 +22,17 @@
|
|||
#= require cal-heatmap
|
||||
#= require turbolinks
|
||||
#= require autosave
|
||||
#= require bootstrap
|
||||
#= require bootstrap/affix
|
||||
#= require bootstrap/alert
|
||||
#= require bootstrap/button
|
||||
#= require bootstrap/collapse
|
||||
#= require bootstrap/dropdown
|
||||
#= require bootstrap/modal
|
||||
#= require bootstrap/scrollspy
|
||||
#= require bootstrap/tab
|
||||
#= require bootstrap/transition
|
||||
#= require bootstrap/tooltip
|
||||
#= require bootstrap/popover
|
||||
#= require select2
|
||||
#= require raphael
|
||||
#= require g.raphael
|
||||
|
@ -41,6 +51,7 @@
|
|||
#= require shortcuts_issuable
|
||||
#= require shortcuts_network
|
||||
#= require jquery.nicescroll
|
||||
#= require date.format
|
||||
#= require_tree .
|
||||
#= require fuzzaldrin-plus
|
||||
#= require cropper
|
||||
|
@ -163,7 +174,7 @@ $ ->
|
|||
$('.trigger-submit').on 'change', ->
|
||||
$(@).parents('form').submit()
|
||||
|
||||
$('abbr.timeago, .js-timeago').timeago()
|
||||
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false)
|
||||
|
||||
# Flash
|
||||
if (flash = $(".flash-container")).length > 0
|
||||
|
|
|
@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
|
|||
e.preventDefault()
|
||||
|
||||
$form = $(e.target).closest('form')
|
||||
$form.find('input[type=submit], button[type=submit]').disable()
|
||||
$submit_button = $form.find('input[type=submit], button[type=submit]')
|
||||
|
||||
return if $submit_button.attr('disabled')
|
||||
|
||||
$submit_button.disable()
|
||||
$form.submit()
|
||||
|
||||
# If the user tabs to a submit button on a `js-quick-submit` form, display a
|
||||
|
|
|
@ -28,26 +28,26 @@ class Dispatcher
|
|||
new Todos()
|
||||
when 'projects:milestones:new', 'projects:milestones:edit'
|
||||
new ZenMode()
|
||||
new DropzoneInput($('.milestone-form'))
|
||||
new GLForm($('.milestone-form'))
|
||||
when 'groups:milestones:new'
|
||||
new ZenMode()
|
||||
when 'projects:compare:show'
|
||||
new Diff()
|
||||
when 'projects:issues:new','projects:issues:edit'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new DropzoneInput($('.issue-form'))
|
||||
new GLForm($('.issue-form'))
|
||||
new IssuableForm($('.issue-form'))
|
||||
when 'projects:merge_requests:new', 'projects:merge_requests:edit'
|
||||
new Diff()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new DropzoneInput($('.merge-request-form'))
|
||||
new GLForm($('.merge-request-form'))
|
||||
new IssuableForm($('.merge-request-form'))
|
||||
when 'projects:tags:new'
|
||||
new ZenMode()
|
||||
new DropzoneInput($('.tag-form'))
|
||||
new GLForm($('.tag-form'))
|
||||
when 'projects:releases:edit'
|
||||
new ZenMode()
|
||||
new DropzoneInput($('.release-form'))
|
||||
new GLForm($('.release-form'))
|
||||
when 'projects:merge_requests:show'
|
||||
new Diff()
|
||||
shortcut_handler = new ShortcutsIssuable(true)
|
||||
|
@ -137,7 +137,7 @@ class Dispatcher
|
|||
new Wikis()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new ZenMode()
|
||||
new DropzoneInput($('.wiki-form'))
|
||||
new GLForm($('.wiki-form'))
|
||||
when 'snippets'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new ZenMode() if path[2] == 'show'
|
||||
|
|
|
@ -15,11 +15,13 @@ class @DropzoneInput
|
|||
project_uploads_path = window.project_uploads_path or null
|
||||
max_file_size = gon.max_file_size or 10
|
||||
|
||||
form_textarea = $(form).find("textarea.markdown-area")
|
||||
form_textarea = $(form).find(".js-gfm-input")
|
||||
form_textarea.wrap "<div class=\"div-dropzone\"></div>"
|
||||
form_textarea.on 'paste', (event) =>
|
||||
handlePaste(event)
|
||||
|
||||
$mdArea = $(form_textarea).closest('.md-area')
|
||||
|
||||
$(form).setupMarkdownPreview()
|
||||
|
||||
form_dropzone = $(form).find('.div-dropzone')
|
||||
|
@ -49,17 +51,16 @@ class @DropzoneInput
|
|||
$(".div-dropzone-alert").alert "close"
|
||||
|
||||
dragover: ->
|
||||
form_textarea.addClass "div-dropzone-focus"
|
||||
$mdArea.addClass 'is-dropzone-hover'
|
||||
form.find(".div-dropzone-hover").css "opacity", 0.7
|
||||
return
|
||||
|
||||
dragleave: ->
|
||||
form_textarea.removeClass "div-dropzone-focus"
|
||||
$mdArea.removeClass 'is-dropzone-hover'
|
||||
form.find(".div-dropzone-hover").css "opacity", 0
|
||||
return
|
||||
|
||||
drop: ->
|
||||
form_textarea.removeClass "div-dropzone-focus"
|
||||
form.find(".div-dropzone-hover").css "opacity", 0
|
||||
form_textarea.focus()
|
||||
return
|
||||
|
|
|
@ -122,7 +122,9 @@ class GitLabDropdown
|
|||
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
|
||||
|
||||
constructor: (@el, @options) ->
|
||||
@dropdown = $(@el).parent()
|
||||
self = @
|
||||
selector = $(@el).data "target"
|
||||
@dropdown = if selector? then $(selector) else $(@el).parent()
|
||||
|
||||
# Set Defaults
|
||||
{
|
||||
|
|
51
app/assets/javascripts/gl_form.js.coffee
Normal file
51
app/assets/javascripts/gl_form.js.coffee
Normal file
|
@ -0,0 +1,51 @@
|
|||
class @GLForm
|
||||
constructor: (@form) ->
|
||||
@textarea = @form.find('textarea.js-gfm-input')
|
||||
|
||||
# Before we start, we should clean up any previous data for this form
|
||||
@destroy()
|
||||
|
||||
# Setup the form
|
||||
@setupForm()
|
||||
|
||||
@form.data 'gl-form', @
|
||||
|
||||
destroy: ->
|
||||
# Clean form listeners
|
||||
@clearEventListeners()
|
||||
@form.data 'gl-form', null
|
||||
|
||||
setupForm: ->
|
||||
isNewForm = @form.is(':not(.gfm-form)')
|
||||
|
||||
@form.removeClass 'js-new-note-form'
|
||||
|
||||
if isNewForm
|
||||
@form.find('.div-dropzone').remove()
|
||||
@form.addClass('gfm-form')
|
||||
disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button')
|
||||
|
||||
# remove notify commit author checkbox for non-commit notes
|
||||
GitLab.GfmAutoComplete.setup()
|
||||
new DropzoneInput(@form)
|
||||
|
||||
autosize(@textarea)
|
||||
|
||||
# form and textarea event listeners
|
||||
@addEventListeners()
|
||||
|
||||
# hide discard button
|
||||
@form.find('.js-note-discard').hide()
|
||||
|
||||
@form.show()
|
||||
|
||||
clearEventListeners: ->
|
||||
@textarea.off 'focus'
|
||||
@textarea.off 'blur'
|
||||
|
||||
addEventListeners: ->
|
||||
@textarea.on 'focus', ->
|
||||
$(@).closest('.md-area').addClass 'is-focused'
|
||||
|
||||
@textarea.on 'blur', ->
|
||||
$(@).closest('.md-area').removeClass 'is-focused'
|
|
@ -10,6 +10,9 @@ class @Issue
|
|||
@initTaskList()
|
||||
@initIssueBtnEventListeners()
|
||||
|
||||
@initMergeRequests()
|
||||
@initRelatedBranches()
|
||||
|
||||
initTaskList: ->
|
||||
$('.detail-page-description .js-task-list-container').taskList('enable')
|
||||
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
|
||||
|
@ -69,3 +72,23 @@ class @Issue
|
|||
type: 'PATCH'
|
||||
url: $('form.js-issuable-update').attr('action')
|
||||
data: patchData
|
||||
|
||||
initMergeRequests: ->
|
||||
$container = $('#merge-requests')
|
||||
|
||||
$.getJSON($container.data('url'))
|
||||
.error ->
|
||||
new Flash('Failed to load referenced merge requests', 'alert')
|
||||
.success (data) ->
|
||||
if 'html' of data
|
||||
$container.html(data.html)
|
||||
|
||||
initRelatedBranches: ->
|
||||
$container = $('#related-branches')
|
||||
|
||||
$.getJSON($container.data('url'))
|
||||
.error ->
|
||||
new Flash('Failed to load related branches', 'alert')
|
||||
.success (data) ->
|
||||
if 'html' of data
|
||||
$container.html(data.html)
|
||||
|
|
|
@ -34,7 +34,7 @@ class @LabelsSelect
|
|||
labelHTMLTemplate = _.template(
|
||||
'<% _.each(labels, function(label){ %>
|
||||
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
|
||||
<span class="label color-label" style="background-color: <%= label.color %>;">
|
||||
<span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;">
|
||||
<%= label.title %>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -165,6 +165,8 @@ class @LabelsSelect
|
|||
.html(template)
|
||||
$sidebarCollapsedValue.text(labelCount)
|
||||
|
||||
$('.has-tooltip', $value).tooltip(container: 'body')
|
||||
|
||||
$value
|
||||
.find('a')
|
||||
.each((i) ->
|
||||
|
@ -218,7 +220,7 @@ class @LabelsSelect
|
|||
selectable: true
|
||||
|
||||
toggleLabel: (selected) ->
|
||||
if selected and selected.title isnt 'Any Label'
|
||||
if selected and selected.title?
|
||||
selected.title
|
||||
else
|
||||
defaultLabel
|
||||
|
|
17
app/assets/javascripts/lib/datetime_utility.js.coffee
Normal file
17
app/assets/javascripts/lib/datetime_utility.js.coffee
Normal file
|
@ -0,0 +1,17 @@
|
|||
((w) ->
|
||||
|
||||
w.gl ?= {}
|
||||
w.gl.utils ?= {}
|
||||
|
||||
w.gl.utils.formatDate = (datetime) ->
|
||||
dateFormat(datetime, 'mmm d, yyyy h:MMtt Z')
|
||||
|
||||
w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) ->
|
||||
$timeagoEls.each( ->
|
||||
$el = $(@)
|
||||
$el.attr('title', gl.utils.formatDate($el.attr('datetime')))
|
||||
)
|
||||
|
||||
$timeagoEls.timeago() if setTimeago
|
||||
|
||||
) window
|
|
@ -85,8 +85,10 @@ class @MergeRequestTabs
|
|||
|
||||
scrollToElement: (container) ->
|
||||
if window.location.hash
|
||||
$el = $("div#{container} #{window.location.hash}")
|
||||
$('body').scrollTo($el.offset().top) if $el.length
|
||||
navBarHeight = $('.navbar-gitlab').outerHeight()
|
||||
|
||||
$el = $("#{container} #{window.location.hash}")
|
||||
$.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length
|
||||
|
||||
# Activate a tab based on the current action
|
||||
activateTab: (action) ->
|
||||
|
@ -142,7 +144,7 @@ class @MergeRequestTabs
|
|||
url: "#{source}.json"
|
||||
success: (data) =>
|
||||
document.querySelector("div#commits").innerHTML = data.html
|
||||
$('.js-timeago').timeago()
|
||||
gl.utils.localTimeAgo($('.js-timeago', 'div#commits'))
|
||||
@commitsLoaded = true
|
||||
@scrollToElement("#commits")
|
||||
|
||||
|
@ -152,12 +154,38 @@ class @MergeRequestTabs
|
|||
@_get
|
||||
url: "#{source}.json" + @_location.search
|
||||
success: (data) =>
|
||||
document.querySelector("div#diffs").innerHTML = data.html
|
||||
$('.js-timeago').timeago()
|
||||
$('div#diffs .js-syntax-highlight').syntaxHighlight()
|
||||
$('#diffs').html data.html
|
||||
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
|
||||
$('#diffs .js-syntax-highlight').syntaxHighlight()
|
||||
@expandViewContainer() if @diffViewType() is 'parallel'
|
||||
@diffsLoaded = true
|
||||
@scrollToElement("#diffs")
|
||||
@highlighSelectedLine()
|
||||
|
||||
$(document)
|
||||
.off 'click', '.diff-line-num a'
|
||||
.on 'click', '.diff-line-num a', (e) =>
|
||||
e.preventDefault()
|
||||
window.location.hash = $(e.currentTarget).attr 'href'
|
||||
@highlighSelectedLine()
|
||||
@scrollToElement("#diffs")
|
||||
|
||||
highlighSelectedLine: ->
|
||||
$('.hll').removeClass 'hll'
|
||||
locationHash = window.location.hash
|
||||
|
||||
if locationHash isnt ''
|
||||
hashClassString = ".#{locationHash.replace('#', '')}"
|
||||
$diffLine = $(locationHash)
|
||||
|
||||
if $diffLine.is ':not(tr)'
|
||||
$diffLine = $("td#{locationHash}, td#{hashClassString}")
|
||||
else
|
||||
$diffLine = $('td', $diffLine)
|
||||
|
||||
$diffLine.addClass 'hll'
|
||||
diffLineTop = $diffLine.offset().top
|
||||
navBarHeight = $('.navbar-gitlab').outerHeight()
|
||||
|
||||
loadBuilds: (source) ->
|
||||
return if @buildsLoaded
|
||||
|
@ -166,7 +194,7 @@ class @MergeRequestTabs
|
|||
url: "#{source}.json"
|
||||
success: (data) =>
|
||||
document.querySelector("div#builds").innerHTML = data.html
|
||||
$('.js-timeago').timeago()
|
||||
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'))
|
||||
@buildsLoaded = true
|
||||
@scrollToElement("#builds")
|
||||
|
||||
|
|
|
@ -163,9 +163,15 @@ class @Notes
|
|||
else if @isNewNote(note)
|
||||
@note_ids.push(note.id)
|
||||
|
||||
$('ul.main-notes-list')
|
||||
$notesList = $('ul.main-notes-list')
|
||||
|
||||
$notesList
|
||||
.append(note.html)
|
||||
.syntaxHighlight()
|
||||
|
||||
# Update datetime format on the recent note
|
||||
gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false)
|
||||
|
||||
@initTaskList()
|
||||
@updateNotesCount(1)
|
||||
|
||||
|
@ -217,6 +223,8 @@ class @Notes
|
|||
# append new note to all matching discussions
|
||||
discussionContainer.append note_html
|
||||
|
||||
gl.utils.localTimeAgo($('.js-timeago', note_html), false)
|
||||
|
||||
@updateNotesCount(1)
|
||||
|
||||
###
|
||||
|
@ -275,32 +283,10 @@ class @Notes
|
|||
show the form
|
||||
###
|
||||
setupNoteForm: (form) ->
|
||||
disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button")
|
||||
form.removeClass "js-new-note-form"
|
||||
form.find('.div-dropzone').remove()
|
||||
|
||||
# hide discard button
|
||||
form.find('.js-note-discard').hide()
|
||||
|
||||
# setup preview buttons
|
||||
previewButton = form.find(".js-md-preview-button")
|
||||
new GLForm form
|
||||
|
||||
textarea = form.find(".js-note-text")
|
||||
|
||||
textarea.on "input", ->
|
||||
if $(this).val().trim() isnt ""
|
||||
previewButton.removeClass("turn-off").addClass "turn-on"
|
||||
else
|
||||
previewButton.removeClass("turn-on").addClass "turn-off"
|
||||
|
||||
textarea.on 'focus', ->
|
||||
$(this).closest('.md-area').addClass 'is-focused'
|
||||
|
||||
textarea.on 'blur', ->
|
||||
$(this).closest('.md-area').removeClass 'is-focused'
|
||||
|
||||
autosize(textarea)
|
||||
|
||||
new Autosave textarea, [
|
||||
"Note"
|
||||
form.find("#note_commit_id").val()
|
||||
|
@ -309,11 +295,6 @@ class @Notes
|
|||
form.find("#note_noteable_id").val()
|
||||
]
|
||||
|
||||
# remove notify commit author checkbox for non-commit notes
|
||||
GitLab.GfmAutoComplete.setup()
|
||||
new DropzoneInput(form)
|
||||
form.show()
|
||||
|
||||
###
|
||||
Called in response to the new note form being submitted
|
||||
|
||||
|
@ -345,7 +326,9 @@ class @Notes
|
|||
updateNote: (_xhr, note, _status) =>
|
||||
# Convert returned HTML to a jQuery object so we can modify it further
|
||||
$html = $(note.html)
|
||||
$('.js-timeago', $html).timeago()
|
||||
|
||||
gl.utils.localTimeAgo($('.js-timeago', $html))
|
||||
|
||||
$html.syntaxHighlight()
|
||||
$html.find('.js-task-list-container').taskList('enable')
|
||||
|
||||
|
@ -365,34 +348,15 @@ class @Notes
|
|||
note = $(this).closest(".note")
|
||||
note.addClass "is-editting"
|
||||
form = note.find(".note-edit-form")
|
||||
isNewForm = form.is(':not(.gfm-form)')
|
||||
if isNewForm
|
||||
form.addClass('gfm-form')
|
||||
|
||||
form.addClass('current-note-edit-form')
|
||||
|
||||
# Show the attachment delete link
|
||||
note.find(".js-note-attachment-delete").show()
|
||||
|
||||
# Setup markdown form
|
||||
if isNewForm
|
||||
GitLab.GfmAutoComplete.setup()
|
||||
new DropzoneInput(form)
|
||||
new GLForm form
|
||||
|
||||
textarea = form.find("textarea")
|
||||
textarea.focus()
|
||||
|
||||
if isNewForm
|
||||
autosize(textarea)
|
||||
|
||||
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
|
||||
# The textarea has the correct value, Chrome just won't show it unless we
|
||||
# modify it, so let's clear it and re-set it!
|
||||
value = textarea.val()
|
||||
textarea.val ""
|
||||
textarea.val value
|
||||
|
||||
if isNewForm
|
||||
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
|
||||
form.find(".js-note-text").focus()
|
||||
|
||||
###
|
||||
Called in response to clicking the edit note link
|
||||
|
@ -549,6 +513,9 @@ class @Notes
|
|||
removeDiscussionNoteForm: (form)->
|
||||
row = form.closest("tr")
|
||||
|
||||
glForm = form.data 'gl-form'
|
||||
glForm.destroy()
|
||||
|
||||
form.find(".js-note-text").data("autosave").reset()
|
||||
|
||||
# show the reply button (will only work for replies)
|
||||
|
@ -560,7 +527,6 @@ class @Notes
|
|||
# only remove the form
|
||||
form.remove()
|
||||
|
||||
|
||||
cancelDiscussionForm: (e) =>
|
||||
e.preventDefault()
|
||||
form = $(e.target).closest(".js-discussion-note-form")
|
||||
|
|
|
@ -18,8 +18,11 @@ class @Profile
|
|||
$(this).find('.btn-save').enable()
|
||||
$(this).find('.loading-gif').hide()
|
||||
|
||||
$('.update-notifications').on 'ajax:complete', ->
|
||||
$(this).find('.btn-save').enable()
|
||||
$('.update-notifications').on 'ajax:success', (e, data) ->
|
||||
if data.saved
|
||||
new Flash("Notification settings saved", "notice")
|
||||
else
|
||||
new Flash("Failed to save new settings", "alert")
|
||||
|
||||
@bindEvents()
|
||||
|
||||
|
|
|
@ -37,19 +37,20 @@ class @Project
|
|||
$('.update-notification').on 'click', (e) ->
|
||||
e.preventDefault()
|
||||
notification_level = $(@).data 'notification-level'
|
||||
$('#notification_level').val(notification_level)
|
||||
label = $(@).data 'notification-title'
|
||||
$('#notification_setting_level').val(notification_level)
|
||||
$('#notification-form').submit()
|
||||
label = null
|
||||
switch notification_level
|
||||
when 0 then label = ' Disabled '
|
||||
when 1 then label = ' Participating '
|
||||
when 2 then label = ' Watching '
|
||||
when 3 then label = ' Global '
|
||||
when 4 then label = ' On Mention '
|
||||
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
|
||||
$(@).parents('ul').find('li.active').removeClass 'active'
|
||||
$(@).parent().addClass 'active'
|
||||
|
||||
$('#notification-form').on 'ajax:success', (e, data) ->
|
||||
if data.saved
|
||||
new Flash("Notification settings saved", "notice")
|
||||
else
|
||||
new Flash("Failed to save new settings", "alert")
|
||||
|
||||
|
||||
@projectSelectDropdown()
|
||||
|
||||
projectSelectDropdown: ->
|
||||
|
|
|
@ -1,5 +1,37 @@
|
|||
class @ProjectSelect
|
||||
constructor: ->
|
||||
$('.js-projects-dropdown-toggle').each (i, dropdown) ->
|
||||
$dropdown = $(dropdown)
|
||||
|
||||
$dropdown.glDropdown(
|
||||
filterable: true
|
||||
filterRemote: true
|
||||
search:
|
||||
fields: ['name_with_namespace']
|
||||
data: (term, callback) ->
|
||||
finalCallback = (projects) ->
|
||||
callback projects
|
||||
|
||||
if @includeGroups
|
||||
projectsCallback = (projects) ->
|
||||
groupsCallback = (groups) ->
|
||||
data = groups.concat(projects)
|
||||
finalCallback(data)
|
||||
|
||||
Api.groups term, false, groupsCallback
|
||||
else
|
||||
projectsCallback = finalCallback
|
||||
|
||||
if @groupId
|
||||
Api.groupProjects @groupId, term, projectsCallback
|
||||
else
|
||||
Api.projects term, @orderBy, projectsCallback
|
||||
url: (project) ->
|
||||
project.web_url
|
||||
text: (project) ->
|
||||
project.name_with_namespace
|
||||
)
|
||||
|
||||
$('.ajax-project-select').each (i, select) ->
|
||||
@groupId = $(select).data('group-id')
|
||||
@includeGroups = $(select).data('include-groups')
|
||||
|
|
|
@ -2,7 +2,7 @@ class @Subscription
|
|||
constructor: (container) ->
|
||||
$container = $(container)
|
||||
@url = $container.attr('data-url')
|
||||
@subscribe_button = $container.find('.subscribe-button')
|
||||
@subscribe_button = $container.find('.js-subscribe-button')
|
||||
@subscription_status = $container.find('.subscription-status')
|
||||
@subscribe_button.unbind('click').click(@toggleSubscription)
|
||||
|
||||
|
|
|
@ -59,6 +59,8 @@ class @Todos
|
|||
|
||||
goToTodoUrl: (e)->
|
||||
todoLink = $(this).data('url')
|
||||
return unless todoLink
|
||||
|
||||
if e.metaKey
|
||||
e.preventDefault()
|
||||
window.open(todoLink,'_blank')
|
||||
|
|
|
@ -1,24 +1,6 @@
|
|||
/**
|
||||
* Styles that apply to all GFM related forms.
|
||||
*/
|
||||
.issue-form, .merge-request-form, .wiki-form {
|
||||
.description {
|
||||
height: 16em;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wiki-form {
|
||||
.description {
|
||||
height: 26em;
|
||||
}
|
||||
}
|
||||
|
||||
.milestone-form {
|
||||
.description {
|
||||
height: 14em;
|
||||
}
|
||||
}
|
||||
|
||||
.gfm-commit, .gfm-commit_range {
|
||||
font-family: $monospace_font;
|
||||
|
|
|
@ -69,6 +69,7 @@ header {
|
|||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
height: $header-height;
|
||||
padding-right: 20px;
|
||||
|
||||
|
@ -76,6 +77,10 @@ header {
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
.div-dropzone-wrapper {
|
||||
.div-dropzone {
|
||||
position: relative;
|
||||
margin-bottom: -5px;
|
||||
|
||||
.div-dropzone-focus {
|
||||
border-color: #66afe9 !important;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important;
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
.div-dropzone-hover {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -0.5em;
|
||||
margin-left: -0.6em;
|
||||
margin-top: -11.5px;
|
||||
margin-left: -15px;
|
||||
opacity: 0;
|
||||
font-size: 50px;
|
||||
font-size: 30px;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -58,12 +58,12 @@
|
|||
|
||||
.nav-search {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
padding: 11px 0;
|
||||
|
||||
/* Small devices (phones, tablets, 768px and lower) */
|
||||
@media (max-width: $screen-sm-min) {
|
||||
width: 100%;
|
||||
@media (min-width: $screen-sm-min) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
@import "bootstrap/modals";
|
||||
@import "bootstrap/tooltip";
|
||||
@import "bootstrap/popovers";
|
||||
@import "bootstrap/carousel";
|
||||
|
||||
// Utility classes
|
||||
.clearfix {
|
||||
|
|
|
@ -250,14 +250,6 @@ a > code {
|
|||
* Textareas intended for GFM
|
||||
*
|
||||
*/
|
||||
.js-gfm-input {
|
||||
font-family: $monospace_font;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.md-preview {
|
||||
}
|
||||
|
||||
.strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ $gl-link-color: #3084bb;
|
|||
$gl-dark-link-color: #333;
|
||||
$gl-placeholder-color: #8f8f8f;
|
||||
$gl-icon-color: $gl-placeholder-color;
|
||||
$gl-grayish-blue: #7f8fa4;
|
||||
$gl-gray: $gl-text-color;
|
||||
$gl-header-color: $gl-title-color;
|
||||
|
||||
|
@ -149,6 +150,7 @@ $light-grey-header: #faf9f9;
|
|||
*/
|
||||
$gl-primary: $blue-normal;
|
||||
$gl-success: $green-normal;
|
||||
$gl-success-focus: rgba($gl-success, .4);
|
||||
$gl-info: $blue-normal;
|
||||
$gl-warning: $orange-normal;
|
||||
$gl-danger: $red-normal;
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #557;
|
||||
border-color: darken(#557, 15%);
|
||||
}
|
||||
|
||||
.diff-line-num.new, .line_content.new {
|
||||
@include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #49483e;
|
||||
border-color: darken(#49483e, 15%);
|
||||
}
|
||||
|
||||
.diff-line-num.new, .line_content.new {
|
||||
@include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #174652;
|
||||
border-color: darken(#174652, 15%);
|
||||
}
|
||||
|
||||
.diff-line-num.new, .line_content.new {
|
||||
@include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #ddd8c5;
|
||||
border-color: darken(#ddd8c5, 15%);
|
||||
}
|
||||
|
||||
.diff-line-num.new, .line_content.new {
|
||||
@include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #f8eec7;
|
||||
border-color: darken(#f8eec7, 15%);
|
||||
}
|
||||
|
||||
.diff-line-num {
|
||||
&.old {
|
||||
background-color: $line-number-old;
|
||||
|
|
|
@ -67,6 +67,24 @@
|
|||
line-height: $code_line_height;
|
||||
font-size: $code_font_size;
|
||||
|
||||
&.noteable_line {
|
||||
position: relative;
|
||||
|
||||
&.old {
|
||||
&:before {
|
||||
content: '-';
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.new {
|
||||
&:before {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
white-space: pre;
|
||||
}
|
||||
|
@ -391,3 +409,23 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-holder {
|
||||
.diff-line-num:not(.js-unfold-bottom) {
|
||||
a {
|
||||
&:before {
|
||||
content: attr(data-linenumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discussion {
|
||||
.diff-content {
|
||||
.diff-line-num {
|
||||
&:before {
|
||||
content: attr(data-linenumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,17 @@
|
|||
word-wrap: break-word;
|
||||
|
||||
.md {
|
||||
color: #7f8fa4;
|
||||
color: $gl-grayish-blue;
|
||||
font-size: $gl-font-size;
|
||||
|
||||
.label {
|
||||
color: $gl-text-color;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
iframe.twitter-share-button {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
|
|
|
@ -173,12 +173,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.subscribe-button {
|
||||
span {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
/* Extra small devices (phones, less than 768px) */
|
||||
display: none;
|
||||
|
@ -263,6 +257,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
padding-top: 6px;
|
||||
|
@ -316,3 +316,9 @@
|
|||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-form-padding-top {
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-top: 7px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,19 +79,30 @@
|
|||
color: $white-light;
|
||||
}
|
||||
|
||||
@mixin labels-mobile {
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.manage-labels-list {
|
||||
|
||||
.prepend-left-10 {
|
||||
.prepend-left-10, .prepend-description-left {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
vertical-align: middle;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
@include labels-mobile;
|
||||
}
|
||||
|
||||
.prepend-description-left {
|
||||
width: 57%;
|
||||
|
||||
@include labels-mobile;
|
||||
}
|
||||
|
||||
.pull-info-right {
|
||||
|
@ -106,7 +117,7 @@
|
|||
padding: 6px;
|
||||
color: $gl-text-color;
|
||||
|
||||
&.subscribe-button {
|
||||
&.label-subscribe-button {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@
|
|||
overflow: hidden;
|
||||
font-size: 90%;
|
||||
margin: 0 3px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mr-list {
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
}
|
||||
|
||||
.note-textarea {
|
||||
display: block;
|
||||
padding: 10px 0;
|
||||
font-family: $regular_font;
|
||||
border: 0;
|
||||
|
@ -63,7 +64,7 @@
|
|||
|
||||
&.is-focused {
|
||||
border-color: $focus-border-color;
|
||||
box-shadow: 0 0 2px rgba(#000, .2),
|
||||
box-shadow: 0 0 2px $black-transparent,
|
||||
0 0 4px rgba($focus-border-color, .4);
|
||||
|
||||
.comment-toolbar,
|
||||
|
@ -71,12 +72,35 @@
|
|||
border-color: $focus-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dropzone-hover {
|
||||
border-color: $gl-success;
|
||||
box-shadow: 0 0 2px $black-transparent,
|
||||
0 0 4px $gl-success-focus;
|
||||
|
||||
.comment-toolbar,
|
||||
.nav-links {
|
||||
border-color: $gl-success;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
code {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
pre {
|
||||
code {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-form {
|
||||
padding: $gl-padding-top $gl-padding;
|
||||
background-color: #fff;
|
||||
background-color: $white-light;
|
||||
}
|
||||
|
||||
.note-edit-form {
|
||||
|
|
|
@ -81,9 +81,15 @@ ul.notes {
|
|||
@include md-typography;
|
||||
|
||||
// On diffs code should wrap nicely and not overflow
|
||||
pre {
|
||||
p {
|
||||
code {
|
||||
white-space: pre;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
pre {
|
||||
code {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,6 +118,10 @@ ul.notes {
|
|||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.note-header {
|
||||
|
@ -127,7 +137,7 @@ ul.notes {
|
|||
margin-right: 10px;
|
||||
}
|
||||
.line_content {
|
||||
white-space: pre-wrap;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,19 +155,27 @@ ul.notes {
|
|||
background: $background-color;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&.notes_line2 {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
border-left: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
&.notes_content {
|
||||
background-color: #fff;
|
||||
background-color: $background-color;
|
||||
border-width: 1px 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
white-space: normal;
|
||||
|
||||
&.parallel {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.notes {
|
||||
background-color: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -258,8 +276,7 @@ ul.notes {
|
|||
|
||||
.diff-file tr.line_holder {
|
||||
@mixin show-add-diff-note {
|
||||
filter: alpha(opacity=100);
|
||||
opacity: 1.0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.add-diff-note {
|
||||
|
@ -273,13 +290,8 @@ ul.notes {
|
|||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 32px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
|
||||
// "hide" it by default
|
||||
opacity: 0.0;
|
||||
filter: alpha(opacity=0);
|
||||
|
||||
display: none;
|
||||
&:hover {
|
||||
background: $gl-info;
|
||||
color: #fff;
|
||||
|
|
|
@ -34,6 +34,11 @@
|
|||
color: #7f8fa4;
|
||||
font-size: $gl-font-size;
|
||||
|
||||
.label {
|
||||
color: $gl-text-color;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #5c5d5e;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
redirect_to admin_runners_path
|
||||
end
|
||||
|
||||
def clear_repository_check_states
|
||||
RepositoryCheck::ClearWorker.perform_async
|
||||
|
||||
redirect_to(
|
||||
admin_application_settings_path,
|
||||
notice: 'Started asynchronous removal of all repository check states.'
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_application_setting
|
||||
|
@ -82,6 +91,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
:akismet_enabled,
|
||||
:akismet_api_key,
|
||||
:email_author_in_body,
|
||||
:repository_checks_enabled,
|
||||
restricted_visibility_levels: [],
|
||||
import_sources: []
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Admin::ProjectsController < Admin::ApplicationController
|
||||
before_action :project, only: [:show, :transfer]
|
||||
before_action :project, only: [:show, :transfer, :repository_check]
|
||||
before_action :group, only: [:show, :transfer]
|
||||
|
||||
def index
|
||||
|
@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController
|
|||
@projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
|
||||
@projects = @projects.with_push if params[:with_push].present?
|
||||
@projects = @projects.abandoned if params[:abandoned].present?
|
||||
@projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present?
|
||||
@projects = @projects.non_archived unless params[:with_archived].present?
|
||||
@projects = @projects.search(params[:name]) if params[:name].present?
|
||||
@projects = @projects.sort(@sort = params[:sort])
|
||||
|
@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController
|
|||
redirect_to admin_namespace_project_path(@project.namespace, @project)
|
||||
end
|
||||
|
||||
def repository_check
|
||||
RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id)
|
||||
|
||||
redirect_to(
|
||||
admin_namespace_project_path(@project.namespace, @project),
|
||||
notice: 'Repository check was triggered.'
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def project
|
||||
|
|
|
@ -3,6 +3,7 @@ require 'fogbugz'
|
|||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Gitlab::CurrentSettings
|
||||
include Gitlab::GonHelper
|
||||
include GitlabRoutingHelper
|
||||
include PageLayoutHelper
|
||||
|
||||
|
@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base
|
|||
before_action :check_password_expiration
|
||||
before_action :check_2fa_requirement
|
||||
before_action :ldap_security_check
|
||||
before_action :sentry_user_context
|
||||
before_action :sentry_context
|
||||
before_action :default_headers
|
||||
before_action :add_gon_variables
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
|
@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
protected
|
||||
|
||||
def sentry_user_context
|
||||
if Rails.env.production? && current_application_settings.sentry_enabled && current_user
|
||||
Raven.user_context(
|
||||
id: current_user.id,
|
||||
email: current_user.email,
|
||||
username: current_user.username,
|
||||
)
|
||||
def sentry_context
|
||||
if Rails.env.production? && current_application_settings.sentry_enabled
|
||||
if current_user
|
||||
Raven.user_context(
|
||||
id: current_user.id,
|
||||
email: current_user.email,
|
||||
username: current_user.username,
|
||||
)
|
||||
end
|
||||
|
||||
Raven.tags_context(program: sentry_program_context)
|
||||
end
|
||||
|
@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def add_gon_variables
|
||||
gon.api_version = API::API.version
|
||||
gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
|
||||
gon.default_issues_tracker = Project.new.default_issue_tracker.to_param
|
||||
gon.max_file_size = current_application_settings.max_attachment_size
|
||||
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
|
||||
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
|
||||
|
||||
if current_user
|
||||
gon.current_user_id = current_user.id
|
||||
gon.api_token = current_user.private_token
|
||||
end
|
||||
end
|
||||
|
||||
def validate_user_service_ticket!
|
||||
return unless signed_in? && session[:service_tickets]
|
||||
|
||||
|
|
16
app/controllers/groups/notification_settings_controller.rb
Normal file
16
app/controllers/groups/notification_settings_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Groups::NotificationSettingsController < Groups::ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def update
|
||||
notification_setting = current_user.notification_settings_for(group)
|
||||
saved = notification_setting.update_attributes(notification_setting_params)
|
||||
|
||||
render json: { saved: saved }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notification_setting_params
|
||||
params.require(:notification_setting).permit(:level)
|
||||
end
|
||||
end
|
|
@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController
|
|||
@last_push = current_user.recent_push if current_user
|
||||
|
||||
@projects = @projects.includes(:namespace)
|
||||
@projects = @projects.sorted_by_activity
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = @projects.sort(@sort = params[:sort])
|
||||
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||
include Gitlab::CurrentSettings
|
||||
include Gitlab::GonHelper
|
||||
include PageLayoutHelper
|
||||
|
||||
before_action :verify_user_oauth_applications_enabled
|
||||
before_action :authenticate_user!
|
||||
before_action :add_gon_variables
|
||||
|
||||
layout 'profile'
|
||||
|
||||
|
|
|
@ -10,6 +10,11 @@ class Profiles::KeysController < Profiles::ApplicationController
|
|||
@key = current_user.keys.find(params[:id])
|
||||
end
|
||||
|
||||
# Back-compat: We need to support this URL since git-annex webapp points to it
|
||||
def new
|
||||
redirect_to profile_keys_path
|
||||
end
|
||||
|
||||
def create
|
||||
@key = current_user.keys.new(key_params)
|
||||
|
||||
|
|
|
@ -1,39 +1,18 @@
|
|||
class Profiles::NotificationsController < Profiles::ApplicationController
|
||||
def show
|
||||
@user = current_user
|
||||
@notification = current_user.notification
|
||||
@project_members = current_user.project_members
|
||||
@group_members = current_user.group_members
|
||||
@group_notifications = current_user.notification_settings.for_groups
|
||||
@project_notifications = current_user.notification_settings.for_projects
|
||||
end
|
||||
|
||||
def update
|
||||
type = params[:notification_type]
|
||||
|
||||
@saved = if type == 'global'
|
||||
current_user.update_attributes(user_params)
|
||||
elsif type == 'group'
|
||||
group_member = current_user.group_members.find(params[:notification_id])
|
||||
group_member.notification_level = params[:notification_level]
|
||||
group_member.save
|
||||
else
|
||||
project_member = current_user.project_members.find(params[:notification_id])
|
||||
project_member.notification_level = params[:notification_level]
|
||||
project_member.save
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if @saved
|
||||
flash[:notice] = "Notification settings saved"
|
||||
else
|
||||
flash[:alert] = "Failed to save new settings"
|
||||
end
|
||||
|
||||
redirect_back_or_default(default: profile_notifications_path)
|
||||
end
|
||||
|
||||
format.js
|
||||
if current_user.update_attributes(user_params)
|
||||
flash[:notice] = "Notification settings saved"
|
||||
else
|
||||
flash[:alert] = "Failed to save new settings"
|
||||
end
|
||||
|
||||
redirect_back_or_default(default: profile_notifications_path)
|
||||
end
|
||||
|
||||
def user_params
|
||||
|
|
|
@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
include IssuableActions
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :issue, only: [:edit, :update, :show]
|
||||
before_action :issue,
|
||||
only: [:edit, :update, :show, :referenced_merge_requests, :related_branches]
|
||||
|
||||
# Allow read any issue
|
||||
before_action :authorize_read_issue!, only: [:show]
|
||||
|
@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
# Allow issues bulk update
|
||||
before_action :authorize_admin_issues!, only: [:bulk_update]
|
||||
|
||||
# Cross-reference merge requests
|
||||
before_action :closed_by_merge_requests, only: [:show]
|
||||
|
||||
respond_to :html
|
||||
|
||||
def index
|
||||
|
@ -65,8 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
@note = @project.notes.new(noteable: @issue)
|
||||
@notes = @issue.notes.nonawards.with_associations.fresh
|
||||
@noteable = @issue
|
||||
@merge_requests = @issue.referenced_merge_requests(current_user)
|
||||
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -118,15 +114,39 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def referenced_merge_requests
|
||||
@merge_requests = @issue.referenced_merge_requests(current_user)
|
||||
@closed_by_merge_requests = @issue.closed_by_merge_requests(current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
html: view_to_html_string('projects/issues/_merge_requests')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def related_branches
|
||||
merge_requests = @issue.referenced_merge_requests(current_user)
|
||||
|
||||
@related_branches = @issue.related_branches -
|
||||
merge_requests.map(&:source_branch)
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
html: view_to_html_string('projects/issues/_related_branches')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
|
||||
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
|
||||
end
|
||||
|
||||
def closed_by_merge_requests
|
||||
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def issue
|
||||
|
|
16
app/controllers/projects/notification_settings_controller.rb
Normal file
16
app/controllers/projects/notification_settings_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Projects::NotificationSettingsController < Projects::ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def update
|
||||
notification_setting = current_user.notification_settings_for(project)
|
||||
saved = notification_setting.update_attributes(notification_setting_params)
|
||||
|
||||
render json: { saved: saved }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notification_setting_params
|
||||
params.require(:notification_setting).permit(:level)
|
||||
end
|
||||
end
|
|
@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def archive
|
||||
RepositoryArchiveCacheWorker.perform_async
|
||||
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
|
||||
head :ok
|
||||
rescue => ex
|
||||
|
|
|
@ -101,14 +101,18 @@ class ProjectsController < Projects::ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if current_user
|
||||
@membership = @project.team.find_member(current_user.id)
|
||||
|
||||
if @membership
|
||||
@notification_setting = current_user.notification_settings_for(@project)
|
||||
end
|
||||
end
|
||||
|
||||
if @project.repository_exists?
|
||||
if @project.empty_repo?
|
||||
render 'projects/empty'
|
||||
else
|
||||
if current_user
|
||||
@membership = @project.team.find_member(current_user.id)
|
||||
end
|
||||
|
||||
render :show
|
||||
end
|
||||
else
|
||||
|
|
|
@ -184,7 +184,7 @@ module ApplicationHelper
|
|||
element = content_tag :time, time.to_s,
|
||||
class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
|
||||
datetime: time.to_time.getutc.iso8601,
|
||||
title: time.in_time_zone.to_s(:medium),
|
||||
title: time.to_time.in_time_zone.to_s(:medium),
|
||||
data: { toggle: 'tooltip', placement: placement, container: 'body' }
|
||||
|
||||
unless skip_js
|
||||
|
|
|
@ -40,10 +40,11 @@ module DiffHelper
|
|||
(unfold) ? 'unfold js-unfold' : ''
|
||||
end
|
||||
|
||||
def diff_line_content(line)
|
||||
def diff_line_content(line, line_type = nil)
|
||||
if line.blank?
|
||||
" ".html_safe
|
||||
else
|
||||
line[0] = ' ' if %w[new old].include?(line_type)
|
||||
line
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,48 +1,48 @@
|
|||
module NotificationsHelper
|
||||
include IconsHelper
|
||||
|
||||
def notification_icon(notification)
|
||||
if notification.disabled?
|
||||
icon('volume-off', class: 'ns-mute')
|
||||
elsif notification.participating?
|
||||
icon('volume-down', class: 'ns-part')
|
||||
elsif notification.watch?
|
||||
icon('volume-up', class: 'ns-watch')
|
||||
else
|
||||
icon('circle-o', class: 'ns-default')
|
||||
def notification_icon_class(level)
|
||||
case level.to_sym
|
||||
when :disabled
|
||||
'microphone-slash'
|
||||
when :participating
|
||||
'volume-up'
|
||||
when :watch
|
||||
'eye'
|
||||
when :mention
|
||||
'at'
|
||||
when :global
|
||||
'globe'
|
||||
end
|
||||
end
|
||||
|
||||
def notification_list_item(notification_level, user_membership)
|
||||
case notification_level
|
||||
when Notification::N_DISABLED
|
||||
update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash')
|
||||
when Notification::N_PARTICIPATING
|
||||
update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up')
|
||||
when Notification::N_WATCH
|
||||
update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye')
|
||||
when Notification::N_MENTION
|
||||
update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at')
|
||||
when Notification::N_GLOBAL
|
||||
update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe')
|
||||
def notification_icon(level, text = nil)
|
||||
icon("#{notification_icon_class(level)} fw", text: text)
|
||||
end
|
||||
|
||||
def notification_title(level)
|
||||
case level.to_sym
|
||||
when :participating
|
||||
'Participate'
|
||||
when :mention
|
||||
'On mention'
|
||||
else
|
||||
# do nothing
|
||||
level.to_s.titlecase
|
||||
end
|
||||
end
|
||||
|
||||
def update_notification_link(notification_level, user_membership, title, icon)
|
||||
content_tag(:li, class: active_level_for(user_membership, notification_level)) do
|
||||
link_to '#', class: 'update-notification', data: { notification_level: notification_level } do
|
||||
icon("#{icon} fw", text: title)
|
||||
def notification_list_item(level, setting)
|
||||
title = notification_title(level)
|
||||
|
||||
data = {
|
||||
notification_level: level,
|
||||
notification_title: title
|
||||
}
|
||||
|
||||
content_tag(:li, class: ('active' if setting.level == level)) do
|
||||
link_to '#', class: 'update-notification', data: data do
|
||||
notification_icon(level, title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notification_label(user_membership)
|
||||
Notification.new(user_membership).to_s
|
||||
end
|
||||
|
||||
def active_level_for(user_membership, level)
|
||||
'active' if user_membership.notification_level == level
|
||||
end
|
||||
end
|
||||
|
|
|
@ -65,21 +65,14 @@ module ProjectsHelper
|
|||
link_to(simple_sanitize(owner.name), user_path(owner))
|
||||
end
|
||||
|
||||
project_link = link_to project_path(project), { class: "project-item-select-holder" } do
|
||||
link_output = simple_sanitize(project.name)
|
||||
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
|
||||
|
||||
if current_user
|
||||
link_output += project_select_tag :project_path,
|
||||
class: "project-item-select js-projects-dropdown",
|
||||
data: { include_groups: false, order_by: 'last_activity_at' }
|
||||
end
|
||||
|
||||
link_output
|
||||
if current_user
|
||||
project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
|
||||
end
|
||||
project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user
|
||||
|
||||
full_title = namespace_link + ' / ' + project_link
|
||||
full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name
|
||||
full_title = "#{namespace_link} / #{project_link}".html_safe
|
||||
full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name
|
||||
|
||||
full_title
|
||||
end
|
||||
|
|
|
@ -20,6 +20,8 @@ module TodosHelper
|
|||
end
|
||||
|
||||
def todo_target_path(todo)
|
||||
return unless todo.target.present?
|
||||
|
||||
anchor = dom_id(todo.note) if todo.note.present?
|
||||
|
||||
if todo.for_commit?
|
||||
|
|
14
app/mailers/repository_check_mailer.rb
Normal file
14
app/mailers/repository_check_mailer.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class RepositoryCheckMailer < BaseMailer
|
||||
def notify(failed_count)
|
||||
if failed_count == 1
|
||||
@message = "One project failed its last repository check"
|
||||
else
|
||||
@message = "#{failed_count} projects failed their last repository check"
|
||||
end
|
||||
|
||||
mail(
|
||||
to: User.admins.pluck(:email),
|
||||
subject: @message
|
||||
)
|
||||
end
|
||||
end
|
|
@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
require_two_factor_authentication: false,
|
||||
two_factor_grace_period: 48,
|
||||
recaptcha_enabled: false,
|
||||
akismet_enabled: false
|
||||
akismet_enabled: false,
|
||||
repository_checks_enabled: true,
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -150,13 +150,11 @@ class Commit
|
|||
end
|
||||
|
||||
def hook_attrs(with_changed_files: false)
|
||||
path_with_namespace = project.path_with_namespace
|
||||
|
||||
data = {
|
||||
id: id,
|
||||
message: safe_message,
|
||||
timestamp: committed_date.xmlschema,
|
||||
url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}",
|
||||
url: Gitlab::UrlBuilder.build(self),
|
||||
author: {
|
||||
name: author_name,
|
||||
email: author_email
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# == Notifiable concern
|
||||
#
|
||||
# Contains notification functionality
|
||||
#
|
||||
module Notifiable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true
|
||||
end
|
||||
|
||||
def notification
|
||||
@notification ||= Notification.new(self)
|
||||
end
|
||||
end
|
|
@ -27,6 +27,7 @@ class Group < Namespace
|
|||
has_many :users, through: :group_members
|
||||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :shared_projects, through: :project_group_links, source: :project
|
||||
has_many :notification_settings, dependent: :destroy, as: :source
|
||||
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validate :visibility_level_allowed_by_projects
|
||||
|
|
|
@ -106,7 +106,7 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
def related_branches
|
||||
project.repository.branch_names.select do |branch|
|
||||
branch.end_with?("-#{iid}")
|
||||
branch =~ /\A#{iid}-(?!\d+-stable)/i
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -151,7 +151,7 @@ class Issue < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def to_branch_name
|
||||
"#{title.parameterize}-#{iid}"
|
||||
"#{iid}-#{title.parameterize}"
|
||||
end
|
||||
|
||||
def can_be_worked_on?(current_user)
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
class Member < ActiveRecord::Base
|
||||
include Sortable
|
||||
include Notifiable
|
||||
include Gitlab::Access
|
||||
|
||||
attr_accessor :raw_invite_token
|
||||
|
@ -56,12 +55,15 @@ class Member < ActiveRecord::Base
|
|||
|
||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
||||
after_create :send_invite, if: :invite?
|
||||
after_create :create_notification_setting, unless: :invite?
|
||||
after_create :post_create_hook, unless: :invite?
|
||||
after_update :post_update_hook, unless: :invite?
|
||||
after_destroy :post_destroy_hook, unless: :invite?
|
||||
|
||||
delegate :name, :username, :email, to: :user, prefix: true
|
||||
|
||||
default_value_for :notification_level, NotificationSetting.levels[:global]
|
||||
|
||||
class << self
|
||||
def find_by_invite_token(invite_token)
|
||||
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
|
||||
|
@ -160,6 +162,14 @@ class Member < ActiveRecord::Base
|
|||
send_invite
|
||||
end
|
||||
|
||||
def create_notification_setting
|
||||
user.notification_settings.find_or_create_for(source)
|
||||
end
|
||||
|
||||
def notification_setting
|
||||
@notification_setting ||= user.notification_settings_for(source)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invite
|
||||
|
|
|
@ -24,7 +24,6 @@ class GroupMember < Member
|
|||
|
||||
# Make sure group member points only to group as it source
|
||||
default_value_for :source_type, SOURCE_TYPE
|
||||
default_value_for :notification_level, Notification::N_GLOBAL
|
||||
validates_format_of :source_type, with: /\ANamespace\z/
|
||||
default_scope { where(source_type: SOURCE_TYPE) }
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ class ProjectMember < Member
|
|||
|
||||
# Make sure project member points only to project as it source
|
||||
default_value_for :source_type, SOURCE_TYPE
|
||||
default_value_for :notification_level, Notification::N_GLOBAL
|
||||
validates_format_of :source_type, with: /\AProject\z/
|
||||
default_scope { where(source_type: SOURCE_TYPE) }
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
validates :target_project, presence: true
|
||||
validates :target_branch, presence: true
|
||||
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
|
||||
validate :validate_branches
|
||||
validate :validate_branches, unless: :allow_broken
|
||||
validate :validate_fork
|
||||
|
||||
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
|
||||
|
@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
if opened? || reopened?
|
||||
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
|
||||
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
|
||||
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
|
||||
if similar_mrs.any?
|
||||
errors.add :validate_branches,
|
||||
|
@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
def hook_attrs
|
||||
attrs = {
|
||||
source: source_project.hook_attrs,
|
||||
source: source_project.try(:hook_attrs),
|
||||
target: target_project.hook_attrs,
|
||||
last_commit: nil,
|
||||
work_in_progress: work_in_progress?
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
class Notification
|
||||
#
|
||||
# Notification levels
|
||||
#
|
||||
N_DISABLED = 0
|
||||
N_PARTICIPATING = 1
|
||||
N_WATCH = 2
|
||||
N_GLOBAL = 3
|
||||
N_MENTION = 4
|
||||
|
||||
attr_accessor :target
|
||||
|
||||
class << self
|
||||
def notification_levels
|
||||
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH]
|
||||
end
|
||||
|
||||
def options_with_labels
|
||||
{
|
||||
disabled: N_DISABLED,
|
||||
participating: N_PARTICIPATING,
|
||||
watch: N_WATCH,
|
||||
mention: N_MENTION,
|
||||
global: N_GLOBAL
|
||||
}
|
||||
end
|
||||
|
||||
def project_notification_levels
|
||||
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(target)
|
||||
@target = target
|
||||
end
|
||||
|
||||
def disabled?
|
||||
target.notification_level == N_DISABLED
|
||||
end
|
||||
|
||||
def participating?
|
||||
target.notification_level == N_PARTICIPATING
|
||||
end
|
||||
|
||||
def watch?
|
||||
target.notification_level == N_WATCH
|
||||
end
|
||||
|
||||
def global?
|
||||
target.notification_level == N_GLOBAL
|
||||
end
|
||||
|
||||
def mention?
|
||||
target.notification_level == N_MENTION
|
||||
end
|
||||
|
||||
def level
|
||||
target.notification_level
|
||||
end
|
||||
|
||||
def to_s
|
||||
case level
|
||||
when N_DISABLED
|
||||
'Disabled'
|
||||
when N_PARTICIPATING
|
||||
'Participating'
|
||||
when N_WATCH
|
||||
'Watching'
|
||||
when N_MENTION
|
||||
'On mention'
|
||||
when N_GLOBAL
|
||||
'Global'
|
||||
else
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
end
|
28
app/models/notification_setting.rb
Normal file
28
app/models/notification_setting.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
class NotificationSetting < ActiveRecord::Base
|
||||
enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
|
||||
|
||||
default_value_for :level, NotificationSetting.levels[:global]
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :source, polymorphic: true
|
||||
|
||||
validates :user, presence: true
|
||||
validates :source, presence: true
|
||||
validates :level, presence: true
|
||||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||
message: "already exists in source",
|
||||
allow_nil: true }
|
||||
|
||||
scope :for_groups, -> { where(source_type: 'Namespace') }
|
||||
scope :for_projects, -> { where(source_type: 'Project') }
|
||||
|
||||
def self.find_or_create_for(source)
|
||||
setting = find_or_initialize_by(source: source)
|
||||
|
||||
unless setting.persisted?
|
||||
setting.save
|
||||
end
|
||||
|
||||
setting
|
||||
end
|
||||
end
|
19
app/models/oauth_access_token.rb
Normal file
19
app/models/oauth_access_token.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: oauth_access_tokens
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# resource_owner_id :integer
|
||||
# application_id :integer
|
||||
# token :string not null
|
||||
# refresh_token :string
|
||||
# expires_in :integer
|
||||
# revoked_at :datetime
|
||||
# created_at :datetime not null
|
||||
# scopes :string
|
||||
#
|
||||
|
||||
class OauthAccessToken < ActiveRecord::Base
|
||||
belongs_to :resource_owner, class_name: 'User'
|
||||
belongs_to :application, class_name: 'Doorkeeper::Application'
|
||||
end
|
|
@ -154,6 +154,7 @@ class Project < ActiveRecord::Base
|
|||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :invited_groups, through: :project_group_links, source: :group
|
||||
has_many :todos, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy, as: :source
|
||||
|
||||
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
|
||||
|
||||
|
@ -388,9 +389,15 @@ class Project < ActiveRecord::Base
|
|||
|
||||
def add_import_job
|
||||
if forked?
|
||||
RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
|
||||
job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
|
||||
else
|
||||
RepositoryImportWorker.perform_async(self.id)
|
||||
job_id = RepositoryImportWorker.perform_async(self.id)
|
||||
end
|
||||
|
||||
if job_id
|
||||
Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
|
||||
else
|
||||
Rails.logger.error "Import job failed to start for #{path_with_namespace}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -82,17 +82,17 @@ class BambooService < CiService
|
|||
end
|
||||
|
||||
def build_info(sha)
|
||||
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
|
||||
url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
|
||||
|
||||
if username.blank? && password.blank?
|
||||
@response = HTTParty.get(parsed_url.to_s, verify: false)
|
||||
@response = HTTParty.get(url, verify: false)
|
||||
else
|
||||
get_url = "#{url}&os_authType=basic"
|
||||
url << '&os_authType=basic'
|
||||
auth = {
|
||||
username: username,
|
||||
password: password,
|
||||
username: username,
|
||||
password: password
|
||||
}
|
||||
@response = HTTParty.get(get_url, verify: false, basic_auth: auth)
|
||||
@response = HTTParty.get(url, verify: false, basic_auth: auth)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -101,11 +101,11 @@ class BambooService < CiService
|
|||
|
||||
if @response.code != 200 || @response['results']['results']['size'] == '0'
|
||||
# If actual build link can't be determined, send user to build summary page.
|
||||
"#{bamboo_url}/browse/#{build_key}"
|
||||
URI.join(bamboo_url, "/browse/#{build_key}").to_s
|
||||
else
|
||||
# If actual build link is available, go to build result page.
|
||||
result_key = @response['results']['results']['result']['planResultKey']['key']
|
||||
"#{bamboo_url}/browse/#{result_key}"
|
||||
URI.join(bamboo_url, "/browse/#{result_key}").to_s
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -134,7 +134,7 @@ class BambooService < CiService
|
|||
return unless supported_events.include?(data[:object_kind])
|
||||
|
||||
# Bamboo requires a GET and does not take any data.
|
||||
self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}",
|
||||
verify: false)
|
||||
url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
|
||||
self.class.get(url, verify: false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ class BuildsEmailService < Service
|
|||
prop_accessor :recipients
|
||||
boolean_accessor :add_pusher
|
||||
boolean_accessor :notify_only_broken_builds
|
||||
validates :recipients, presence: true, if: :activated?
|
||||
validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
|
||||
|
||||
def initialize_properties
|
||||
if properties.nil?
|
||||
|
@ -87,10 +87,14 @@ class BuildsEmailService < Service
|
|||
end
|
||||
|
||||
def all_recipients(data)
|
||||
all_recipients = recipients.split(',').compact.reject(&:blank?)
|
||||
all_recipients = []
|
||||
|
||||
unless recipients.blank?
|
||||
all_recipients += recipients.split(',').compact.reject(&:blank?)
|
||||
end
|
||||
|
||||
if add_pusher? && data[:user][:email]
|
||||
all_recipients << "#{data[:user][:email]}"
|
||||
all_recipients << data[:user][:email]
|
||||
end
|
||||
|
||||
all_recipients
|
||||
|
|
|
@ -85,13 +85,15 @@ class TeamcityService < CiService
|
|||
end
|
||||
|
||||
def build_info(sha)
|
||||
url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\
|
||||
"branch:unspecified:any,number:#{sha}")
|
||||
url = URI.join(
|
||||
teamcity_url,
|
||||
"/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
|
||||
).to_s
|
||||
auth = {
|
||||
username: username,
|
||||
password: password,
|
||||
password: password
|
||||
}
|
||||
@response = HTTParty.get("#{url}", verify: false, basic_auth: auth)
|
||||
@response = HTTParty.get(url, verify: false, basic_auth: auth)
|
||||
end
|
||||
|
||||
def build_page(sha, ref)
|
||||
|
@ -100,12 +102,14 @@ class TeamcityService < CiService
|
|||
if @response.code != 200
|
||||
# If actual build link can't be determined,
|
||||
# send user to build summary page.
|
||||
"#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}"
|
||||
URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
|
||||
else
|
||||
# If actual build link is available, go to build result page.
|
||||
built_id = @response['build']['id']
|
||||
"#{teamcity_url}/viewLog.html?buildId=#{built_id}"\
|
||||
"&buildTypeId=#{build_type}"
|
||||
URI.join(
|
||||
teamcity_url,
|
||||
"/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
|
||||
).to_s
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -140,12 +144,13 @@ class TeamcityService < CiService
|
|||
|
||||
branch = Gitlab::Git.ref_name(data[:ref])
|
||||
|
||||
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
|
||||
body: "<build branchName=\"#{branch}\">"\
|
||||
"<buildType id=\"#{build_type}\"/>"\
|
||||
'</build>',
|
||||
headers: { 'Content-type' => 'application/xml' },
|
||||
basic_auth: auth
|
||||
)
|
||||
self.class.post(
|
||||
URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
|
||||
body: "<build branchName=\"#{branch}\">"\
|
||||
"<buildType id=\"#{build_type}\"/>"\
|
||||
'</build>',
|
||||
headers: { 'Content-type' => 'application/xml' },
|
||||
basic_auth: auth
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -253,6 +253,8 @@ class Repository
|
|||
# This ensures this particular cache is flushed after the first commit to a
|
||||
# new repository.
|
||||
expire_emptiness_caches if empty?
|
||||
expire_branch_count_cache
|
||||
expire_tag_count_cache
|
||||
end
|
||||
|
||||
def expire_branch_cache(branch_name = nil)
|
||||
|
@ -795,7 +797,7 @@ class Repository
|
|||
|
||||
def search_files(query, ref)
|
||||
offset = 2
|
||||
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
|
||||
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref})
|
||||
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
|
||||
end
|
||||
|
||||
|
|
|
@ -143,6 +143,7 @@ class User < ActiveRecord::Base
|
|||
has_many :spam_logs, dependent: :destroy
|
||||
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
|
||||
has_many :todos, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
|
||||
#
|
||||
# Validations
|
||||
|
@ -157,7 +158,7 @@ class User < ActiveRecord::Base
|
|||
presence: true,
|
||||
uniqueness: { case_sensitive: false }
|
||||
|
||||
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
|
||||
validates :notification_level, presence: true
|
||||
validate :namespace_uniq, if: ->(user) { user.username_changed? }
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validate :unique_email, if: ->(user) { user.email_changed? }
|
||||
|
@ -190,6 +191,13 @@ class User < ActiveRecord::Base
|
|||
# Note: When adding an option, it MUST go on the end of the array.
|
||||
enum project_view: [:readme, :activity, :files]
|
||||
|
||||
# Notification level
|
||||
# Note: When adding an option, it MUST go on the end of the array.
|
||||
#
|
||||
# TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
|
||||
# Because user.notification_disabled? is much better than user.disabled?
|
||||
enum notification_level: [:disabled, :participating, :watch, :global, :mention]
|
||||
|
||||
alias_attribute :private_token, :authentication_token
|
||||
|
||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||
|
@ -349,10 +357,6 @@ class User < ActiveRecord::Base
|
|||
"#{self.class.reference_prefix}#{username}"
|
||||
end
|
||||
|
||||
def notification
|
||||
@notification ||= Notification.new(self)
|
||||
end
|
||||
|
||||
def generate_password
|
||||
if self.force_random_password
|
||||
self.password = self.password_confirmation = Devise.friendly_token.first(8)
|
||||
|
@ -827,6 +831,10 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def notification_settings_for(source)
|
||||
notification_settings.find_or_initialize_by(source: source)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_union
|
||||
|
|
|
@ -3,7 +3,7 @@ module Issues
|
|||
|
||||
def hook_data(issue, action)
|
||||
issue_data = issue.to_hook_data(current_user)
|
||||
issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
|
||||
issue_url = Gitlab::UrlBuilder.build(issue)
|
||||
issue_data[:object_attributes].merge!(url: issue_url, action: action)
|
||||
issue_data
|
||||
end
|
||||
|
|
|
@ -20,8 +20,7 @@ module MergeRequests
|
|||
|
||||
def hook_data(merge_request, action)
|
||||
hook_data = merge_request.to_hook_data(current_user)
|
||||
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
|
||||
hook_data[:object_attributes][:url] = merge_request_url
|
||||
hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
|
||||
hook_data[:object_attributes][:action] = action
|
||||
hook_data
|
||||
end
|
||||
|
|
|
@ -51,7 +51,7 @@ module MergeRequests
|
|||
# be interpreted as the use wants to close that issue on this project
|
||||
# Pattern example: 112-fix-mep-mep
|
||||
# Will lead to appending `Closes #112` to the description
|
||||
if match = merge_request.source_branch.match(/-(\d+)\z/)
|
||||
if match = merge_request.source_branch.match(/\A(\d+)-/)
|
||||
iid = match[1]
|
||||
closes_issue = "Closes ##{iid}"
|
||||
|
||||
|
|
|
@ -253,8 +253,8 @@ class NotificationService
|
|||
def project_watchers(project)
|
||||
project_members = project_member_notification(project)
|
||||
|
||||
users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL)
|
||||
users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL)
|
||||
users_with_project_level_global = project_member_notification(project, :global)
|
||||
users_with_group_level_global = group_member_notification(project, :global)
|
||||
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
|
||||
|
||||
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
|
||||
|
@ -264,18 +264,16 @@ class NotificationService
|
|||
end
|
||||
|
||||
def project_member_notification(project, notification_level=nil)
|
||||
project_members = project.project_members
|
||||
|
||||
if notification_level
|
||||
project_members.where(notification_level: notification_level).pluck(:user_id)
|
||||
project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
|
||||
else
|
||||
project_members.pluck(:user_id)
|
||||
project.notification_settings.pluck(:user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def group_member_notification(project, notification_level)
|
||||
if project.group
|
||||
project.group.group_members.where(notification_level: notification_level).pluck(:user_id)
|
||||
project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
@ -284,13 +282,13 @@ class NotificationService
|
|||
def users_with_global_level_watch(ids)
|
||||
User.where(
|
||||
id: ids,
|
||||
notification_level: Notification::N_WATCH
|
||||
notification_level: NotificationSetting.levels[:watch]
|
||||
).pluck(:id)
|
||||
end
|
||||
|
||||
# Build a list of users based on project notifcation settings
|
||||
def select_project_member_setting(project, global_setting, users_global_level_watch)
|
||||
users = project_member_notification(project, Notification::N_WATCH)
|
||||
users = project_member_notification(project, :watch)
|
||||
|
||||
# If project setting is global, add to watch list if global setting is watch
|
||||
global_setting.each do |user_id|
|
||||
|
@ -304,7 +302,7 @@ class NotificationService
|
|||
|
||||
# Build a list of users based on group notification settings
|
||||
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
|
||||
uids = group_member_notification(project, Notification::N_WATCH)
|
||||
uids = group_member_notification(project, :watch)
|
||||
|
||||
# Group setting is watch, add to users list if user is not project member
|
||||
users = []
|
||||
|
@ -331,40 +329,46 @@ class NotificationService
|
|||
# Remove users with disabled notifications from array
|
||||
# Also remove duplications and nil recipients
|
||||
def reject_muted_users(users, project = nil)
|
||||
reject_users(users, :disabled?, project)
|
||||
reject_users(users, :disabled, project)
|
||||
end
|
||||
|
||||
# Remove users with notification level 'Mentioned'
|
||||
def reject_mention_users(users, project = nil)
|
||||
reject_users(users, :mention?, project)
|
||||
reject_users(users, :mention, project)
|
||||
end
|
||||
|
||||
# Reject users which method_name from notification object returns true.
|
||||
# Reject users which has certain notification level
|
||||
#
|
||||
# Example:
|
||||
# reject_users(users, :watch?, project)
|
||||
# reject_users(users, :watch, project)
|
||||
#
|
||||
def reject_users(users, method_name, project = nil)
|
||||
def reject_users(users, level, project = nil)
|
||||
level = level.to_s
|
||||
|
||||
unless NotificationSetting.levels.keys.include?(level)
|
||||
raise 'Invalid notification level'
|
||||
end
|
||||
|
||||
users = users.to_a.compact.uniq
|
||||
users = users.reject(&:blocked?)
|
||||
|
||||
users.reject do |user|
|
||||
next user.notification.send(method_name) unless project
|
||||
next user.notification_level == level unless project
|
||||
|
||||
member = project.project_members.find_by(user_id: user.id)
|
||||
setting = user.notification_settings_for(project)
|
||||
|
||||
if !member && project.group
|
||||
member = project.group.group_members.find_by(user_id: user.id)
|
||||
if !setting && project.group
|
||||
setting = user.notification_settings_for(project.group)
|
||||
end
|
||||
|
||||
# reject users who globally set mention notification and has no membership
|
||||
next user.notification.send(method_name) unless member
|
||||
# reject users who globally set mention notification and has no setting per project/group
|
||||
next user.notification_level == level unless setting
|
||||
|
||||
# reject users who set mention notification in project
|
||||
next true if member.notification.send(method_name)
|
||||
next true if setting.level == level
|
||||
|
||||
# reject users who have N_MENTION in project and disabled in global settings
|
||||
member.notification.global? && user.notification.send(method_name)
|
||||
# reject users who have mention level in project and disabled in global settings
|
||||
setting.global? && user.notification_level == level
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -26,7 +26,9 @@ module Projects
|
|||
|
||||
GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
|
||||
ensure
|
||||
@project.update_column(:pushes_since_gc, 0)
|
||||
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
|
||||
@project.update_column(:pushes_since_gc, 0)
|
||||
end
|
||||
end
|
||||
|
||||
def needed?
|
||||
|
@ -34,14 +36,18 @@ module Projects
|
|||
end
|
||||
|
||||
def increment!
|
||||
@project.increment!(:pushes_since_gc)
|
||||
Gitlab::Metrics.measure(:increment_pushes_since_gc) do
|
||||
@project.increment!(:pushes_since_gc)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def try_obtain_lease
|
||||
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
|
||||
lease.try_obtain
|
||||
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
|
||||
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
|
||||
lease.try_obtain
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,8 +34,9 @@ module Projects
|
|||
raise TransferError.new("Project with same path in target namespace already exists")
|
||||
end
|
||||
|
||||
# Apply new namespace id
|
||||
# Apply new namespace id and visibility level
|
||||
project.namespace = new_namespace
|
||||
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
|
||||
project.save!
|
||||
|
||||
# Notifications
|
||||
|
@ -56,7 +57,7 @@ module Projects
|
|||
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
|
||||
|
||||
project.old_path_with_namespace = old_path
|
||||
|
||||
|
||||
SystemHooksService.new.execute_hooks_for(project, :transfer)
|
||||
true
|
||||
end
|
||||
|
|
|
@ -222,7 +222,7 @@ class SystemNoteService
|
|||
# Called when a branch is created from the 'new branch' button on a issue
|
||||
# Example note text:
|
||||
#
|
||||
# "Started branch `issue-branch-button-201`"
|
||||
# "Started branch `201-issue-branch-button`"
|
||||
def self.new_issue_branch(issue, project, author, branch)
|
||||
h = Gitlab::Routing.url_helpers
|
||||
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
|
||||
|
|
|
@ -271,5 +271,24 @@
|
|||
.col-sm-10
|
||||
= f.text_field :sentry_dsn, class: 'form-control'
|
||||
|
||||
%fieldset
|
||||
%legend Repository Checks
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
.checkbox
|
||||
= f.label :repository_checks_enabled do
|
||||
= f.check_box :repository_checks_enabled
|
||||
Enable Repository Checks
|
||||
.help-block
|
||||
GitLab will periodically run
|
||||
%a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
|
||||
in all project and wiki repositories to look for silent disk corruption issues.
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
= link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
|
||||
.help-block
|
||||
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
|
||||
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Save', class: 'btn btn-save'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- page_title "Logs"
|
||||
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
|
||||
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
|
||||
Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
|
||||
Gitlab::RepositoryCheckLogger]
|
||||
%ul.nav-links.log-tabs
|
||||
- loggers.each do |klass|
|
||||
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.row.prepend-top-default
|
||||
%aside.col-md-3
|
||||
.admin-filter
|
||||
.panel.admin-filter
|
||||
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
|
||||
.form-group
|
||||
= label_tag :name, 'Name:'
|
||||
|
@ -38,7 +38,13 @@
|
|||
%span.descr
|
||||
= visibility_level_icon(level)
|
||||
= label
|
||||
%hr
|
||||
%fieldset
|
||||
%strong Problems
|
||||
.checkbox
|
||||
= label_tag :last_repository_check_failed do
|
||||
= check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed]
|
||||
%span Last repository check failed
|
||||
|
||||
= hidden_field_tag :sort, params[:sort]
|
||||
= button_tag "Search", class: "btn submit btn-primary"
|
||||
= link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
|
||||
|
|
|
@ -5,6 +5,16 @@
|
|||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
%hr
|
||||
- if @project.last_repository_check_failed?
|
||||
.row
|
||||
.col-md-12
|
||||
.panel
|
||||
.panel-heading.alert.alert-danger
|
||||
Last repository check
|
||||
= "(#{time_ago_in_words(@project.last_repository_check_at)} ago)"
|
||||
failed. See
|
||||
= link_to 'repocheck.log', admin_logs_path
|
||||
for error messages.
|
||||
.row
|
||||
.col-md-6
|
||||
.panel.panel-default
|
||||
|
@ -95,6 +105,32 @@
|
|||
.col-sm-offset-2.col-sm-10
|
||||
= f.submit 'Transfer', class: 'btn btn-primary'
|
||||
|
||||
.panel.panel-default.repository-check
|
||||
.panel-heading
|
||||
Repository check
|
||||
.panel-body
|
||||
= form_for @project, url: repository_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f|
|
||||
.form-group
|
||||
- if @project.last_repository_check_at.nil?
|
||||
This repository has never been checked.
|
||||
- else
|
||||
This repository was last checked
|
||||
= @project.last_repository_check_at.to_s(:medium) + '.'
|
||||
The check
|
||||
- if @project.last_repository_check_failed?
|
||||
= succeed '.' do
|
||||
%strong.cred failed
|
||||
See
|
||||
= link_to 'repocheck.log', admin_logs_path
|
||||
for error messages.
|
||||
- else
|
||||
passed.
|
||||
|
||||
= link_to icon('question-circle'), help_page_path('administration', 'repository_checks')
|
||||
|
||||
.form-group
|
||||
= f.submit 'Trigger repository check', class: 'btn btn-primary'
|
||||
|
||||
.col-md-6
|
||||
- if @group
|
||||
.panel.panel-default
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
%td= app.name
|
||||
%td= token.created_at
|
||||
%td= token.scopes
|
||||
%td= render 'delete_form', application: app
|
||||
%td= render 'doorkeeper/authorized_applications/delete_form', application: app
|
||||
- @authorized_anonymous_tokens.each do |token|
|
||||
%tr
|
||||
%td
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
This will create milestone in every selected project
|
||||
%hr
|
||||
|
||||
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
|
||||
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
|
||||
.row
|
||||
- if @milestone.errors.any?
|
||||
#error_explanation
|
||||
|
@ -27,7 +27,7 @@
|
|||
= f.label :description, "Description", class: "control-label"
|
||||
.col-sm-10
|
||||
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
|
||||
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
|
||||
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
|
||||
.clearfix
|
||||
.error-alert
|
||||
.form-group
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
.navbar-collapse.collapse
|
||||
%ul.nav.navbar-nav
|
||||
%li.hidden-sm.hidden-xs
|
||||
= render 'layouts/search'
|
||||
= render 'layouts/search' unless current_controller?(:search)
|
||||
%li.visible-sm.visible-xs
|
||||
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= icon('search')
|
||||
|
@ -45,6 +45,8 @@
|
|||
|
||||
%h1.title= title
|
||||
|
||||
= yield :header_content
|
||||
|
||||
= render 'shared/outdated_browser'
|
||||
|
||||
- if @project && !@project.empty_repo?
|
||||
|
|
|
@ -17,4 +17,12 @@
|
|||
- content_for :scripts_body do
|
||||
= render "layouts/init_auto_complete" if current_user
|
||||
|
||||
- content_for :header_content do
|
||||
.js-dropdown-menu-projects
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-projects
|
||||
= dropdown_title("Go to a project")
|
||||
= dropdown_filter("Search your projects")
|
||||
= dropdown_content
|
||||
= dropdown_loading
|
||||
|
||||
= render template: "layouts/application"
|
||||
|
|
13
app/views/profiles/notifications/_group_settings.html.haml
Normal file
13
app/views/profiles/notifications/_group_settings.html.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
- if setting.global?
|
||||
= notification_icon(current_user.notification_level)
|
||||
- else
|
||||
= notification_icon(setting.level)
|
||||
|
||||
%span.str-truncated
|
||||
= link_to group.name, group_path(group)
|
||||
|
||||
.pull-right
|
||||
= form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
|
||||
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
|
13
app/views/profiles/notifications/_project_settings.html.haml
Normal file
13
app/views/profiles/notifications/_project_settings.html.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
- if setting.global?
|
||||
= notification_icon(current_user.notification_level)
|
||||
- else
|
||||
= notification_icon(setting.level)
|
||||
|
||||
%span.str-truncated
|
||||
= link_to_project(project)
|
||||
|
||||
.pull-right
|
||||
= form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
|
||||
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
|
|
@ -1,17 +0,0 @@
|
|||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
- if notification.global?
|
||||
= notification_icon(@notification)
|
||||
- else
|
||||
= notification_icon(notification)
|
||||
|
||||
%span.str-truncated
|
||||
- if membership.kind_of? GroupMember
|
||||
= link_to membership.group.name, membership.group
|
||||
- else
|
||||
= link_to_project(membership.project)
|
||||
.pull-right
|
||||
= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
|
||||
= hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type')
|
||||
= hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id')
|
||||
= select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit'
|
|
@ -1,8 +1,12 @@
|
|||
- page_title "Notifications"
|
||||
- header_title page_title, profile_notifications_path
|
||||
|
||||
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
|
||||
= form_errors(@user)
|
||||
%div
|
||||
- if @user.errors.any?
|
||||
%div.alert.alert-danger
|
||||
%ul
|
||||
- @user.errors.full_messages.each do |msg|
|
||||
%li= msg
|
||||
|
||||
= hidden_field_tag :notification_type, 'global'
|
||||
.row
|
||||
|
@ -16,56 +20,55 @@
|
|||
.col-lg-9
|
||||
%h5
|
||||
Global notification settings
|
||||
.form-group
|
||||
= f.label :notification_email, class: "label-light"
|
||||
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
|
||||
.form-group
|
||||
= f.label :notification_level, class: 'label-light'
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_DISABLED do
|
||||
= f.radio_button :notification_level, Notification::N_DISABLED
|
||||
.level-title
|
||||
Disabled
|
||||
%p You will not get any notifications via email
|
||||
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_MENTION do
|
||||
= f.radio_button :notification_level, Notification::N_MENTION
|
||||
.level-title
|
||||
On Mention
|
||||
%p You will receive notifications only for comments in which you were @mentioned
|
||||
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
|
||||
.form-group
|
||||
= f.label :notification_email, class: "label-light"
|
||||
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
|
||||
.form-group
|
||||
= f.label :notification_level, class: 'label-light'
|
||||
.radio
|
||||
= f.label :notification_level, value: :disabled do
|
||||
= f.radio_button :notification_level, :disabled
|
||||
.level-title
|
||||
Disabled
|
||||
%p You will not get any notifications via email
|
||||
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_PARTICIPATING do
|
||||
= f.radio_button :notification_level, Notification::N_PARTICIPATING
|
||||
.level-title
|
||||
Participating
|
||||
%p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
|
||||
.radio
|
||||
= f.label :notification_level, value: :mention do
|
||||
= f.radio_button :notification_level, :mention
|
||||
.level-title
|
||||
On Mention
|
||||
%p You will receive notifications only for comments in which you were @mentioned
|
||||
|
||||
.radio
|
||||
= f.label :notification_level, value: Notification::N_WATCH do
|
||||
= f.radio_button :notification_level, Notification::N_WATCH
|
||||
.level-title
|
||||
Watch
|
||||
%p You will receive notifications for any activity
|
||||
.radio
|
||||
= f.label :notification_level, value: :participating do
|
||||
= f.radio_button :notification_level, :participating
|
||||
.level-title
|
||||
Participating
|
||||
%p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
|
||||
|
||||
.prepend-top-default
|
||||
= f.submit 'Update settings', class: "btn btn-create"
|
||||
.radio
|
||||
= f.label :notification_level, value: :watch do
|
||||
= f.radio_button :notification_level, :watch
|
||||
.level-title
|
||||
Watch
|
||||
%p You will receive notifications for any activity
|
||||
|
||||
.prepend-top-default
|
||||
= f.submit 'Update settings', class: "btn btn-create"
|
||||
%hr
|
||||
.col-lg-9.col-lg-push-3
|
||||
%h5
|
||||
Groups (#{@group_members.count})
|
||||
%div
|
||||
%ul.bordered-list
|
||||
- @group_members.each do |group_member|
|
||||
- notification = Notification.new(group_member)
|
||||
= render 'settings', type: 'group', membership: group_member, notification: notification
|
||||
%h5
|
||||
Projects (#{@project_members.count})
|
||||
%p.account-well
|
||||
To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
|
||||
.append-bottom-default
|
||||
%ul.bordered-list
|
||||
- @project_members.each do |project_member|
|
||||
- notification = Notification.new(project_member)
|
||||
= render 'settings', type: 'project', membership: project_member, notification: notification
|
||||
%h5
|
||||
Groups (#{@group_notifications.count})
|
||||
%div
|
||||
%ul.bordered-list
|
||||
- @group_notifications.each do |setting|
|
||||
= render 'group_settings', setting: setting, group: setting.source
|
||||
%h5
|
||||
Projects (#{@project_notifications.count})
|
||||
%p.account-well
|
||||
To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there.
|
||||
.append-bottom-default
|
||||
%ul.bordered-list
|
||||
- @project_notifications.each do |setting|
|
||||
= render 'project_settings', setting: setting, project: setting.source
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
- if @saved
|
||||
:plain
|
||||
new Flash("Notification settings saved", "notice")
|
||||
- else
|
||||
:plain
|
||||
new Flash("Failed to save new settings", "alert")
|
|
@ -1,8 +1,8 @@
|
|||
.zen-backdrop
|
||||
- classes << ' js-gfm-input js-autosize markdown-area'
|
||||
- if defined?(f) && f
|
||||
= f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..."
|
||||
= f.text_area attr, class: classes, placeholder: placeholder
|
||||
- else
|
||||
= text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..."
|
||||
= text_area_tag attr, nil, class: classes, placeholder: placeholder
|
||||
%a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
|
||||
= icon('compress')
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
- if @lines.present?
|
||||
- if @form.unfold? && @form.since != 1 && !@form.bottom?
|
||||
%tr.line_holder{ id: @form.since }
|
||||
= render "projects/diffs/match_line", {line: @match_line,
|
||||
line_old: @form.since, line_new: @form.since, bottom: false, new_file: false}
|
||||
= render "projects/diffs/match_line", { line: @match_line,
|
||||
line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
|
||||
|
||||
- @lines.each_with_index do |line, index|
|
||||
- line_new = index + @form.since
|
||||
- line_old = line_new - @form.offset
|
||||
%tr.line_holder
|
||||
%td.old_line.diff-line-num{data: {linenumber: line_old}}
|
||||
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
|
||||
= link_to raw(line_old), "#"
|
||||
%td.new_line.diff-line-num
|
||||
%td.new_line.diff-line-num{ data: { linenumber: line_old } }
|
||||
= link_to raw(line_new) , "#"
|
||||
%td.line_content.noteable_line==#{' ' * @form.indent}#{line}
|
||||
|
||||
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
|
||||
%tr.line_holder{ id: @form.to }
|
||||
= render "projects/diffs/match_line", {line: @match_line,
|
||||
line_old: @form.to, line_new: @form.to, bottom: true, new_file: false}
|
||||
= render "projects/diffs/match_line", { line: @match_line,
|
||||
line_old: @form.to, line_new: @form.to, bottom: true, new_file: false }
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
- case @membership
|
||||
- when ProjectMember
|
||||
= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do
|
||||
= hidden_field_tag :notification_type, 'project'
|
||||
= hidden_field_tag :notification_id, @membership.id
|
||||
= hidden_field_tag :notification_level
|
||||
- if @notification_setting
|
||||
= form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
|
||||
= f.hidden_field :level
|
||||
%span.dropdown
|
||||
%a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
|
||||
= icon('bell')
|
||||
= notification_label(@membership)
|
||||
= notification_title(@notification_setting.level)
|
||||
= icon('angle-down')
|
||||
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
|
||||
- Notification.project_notification_levels.each do |level|
|
||||
= notification_list_item(level, @membership)
|
||||
|
||||
- when GroupMember
|
||||
.btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
|
||||
= icon('bell')
|
||||
= notification_label(@membership)
|
||||
= icon('angle-down')
|
||||
- NotificationSetting.levels.each do |level|
|
||||
= notification_list_item(level.first, @notification_setting)
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
- type = line.type
|
||||
%tr.line_holder{id: line_code, class: type}
|
||||
%tr.line_holder{ id: line_code, class: type }
|
||||
- case type
|
||||
- when 'match'
|
||||
= render "projects/diffs/match_line", {line: line.text,
|
||||
line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
|
||||
= render "projects/diffs/match_line", { line: line.text,
|
||||
line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file }
|
||||
- when 'nonewline'
|
||||
%td.old_line.diff-line-num
|
||||
%td.new_line.diff-line-num
|
||||
%td.line_content.match= line.text
|
||||
- else
|
||||
%td.old_line.diff-line-num{class: type}
|
||||
- link_text = raw(type == "new" ? " " : line.old_pos)
|
||||
%td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
|
||||
- link_text = type == "new" ? " ".html_safe : line.old_pos
|
||||
- if defined?(plain) && plain
|
||||
= link_text
|
||||
- else
|
||||
= link_to link_text, "##{line_code}", id: line_code
|
||||
= link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
|
||||
- if @comments_allowed && can?(current_user, :create_note, @project)
|
||||
= link_to_new_diff_note(line_code)
|
||||
%td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
|
||||
- link_text = raw(type == "old" ? " " : line.new_pos)
|
||||
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
|
||||
- link_text = type == "old" ? " ".html_safe : line.new_pos
|
||||
- if defined?(plain) && plain
|
||||
= link_text
|
||||
- else
|
||||
= link_to link_text, "##{line_code}", id: line_code
|
||||
%td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
|
||||
= link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
|
||||
%td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type)
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
%td.new_line.diff-line-num
|
||||
%td.line_content.parallel.match= left[:text]
|
||||
- else
|
||||
%td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"}
|
||||
%td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"}
|
||||
= link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
|
||||
- if @comments_allowed && can?(current_user, :create_note, @project)
|
||||
= link_to_new_diff_note(left[:line_code], 'old')
|
||||
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
|
||||
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
|
||||
|
||||
- if right[:type] == 'new'
|
||||
- new_line_class = 'new'
|
||||
|
@ -27,11 +27,11 @@
|
|||
- new_line_class = nil
|
||||
- new_line_code = left[:line_code]
|
||||
|
||||
%td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }}
|
||||
%td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }}
|
||||
= link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
|
||||
- if @comments_allowed && can?(current_user, :create_note, @project)
|
||||
= link_to_new_diff_note(right[:line_code], 'new')
|
||||
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
|
||||
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
|
||||
|
||||
- if @reply_allowed
|
||||
- comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue