summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG20
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee12
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee9
-rw-r--r--app/assets/javascripts/gl_form.js.coffee51
-rw-r--r--app/assets/javascripts/issue.js.coffee23
-rw-r--r--app/assets/javascripts/labels_select.js.coffee4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee36
-rw-r--r--app/assets/javascripts/notes.js.coffee58
-rw-r--r--app/assets/javascripts/subscription.js.coffee2
-rw-r--r--app/assets/javascripts/users_select.js.coffee3
-rw-r--r--app/assets/stylesheets/framework/gfm.scss18
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss13
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/highlight/dark.scss6
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss6
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss6
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss6
-rw-r--r--app/assets/stylesheets/highlight/white.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss38
-rw-r--r--app/assets/stylesheets/pages/issuable.scss12
-rw-r--r--app/assets/stylesheets/pages/labels.scss27
-rw-r--r--app/assets/stylesheets/pages/note_form.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss10
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/admin/projects_controller.rb12
-rw-r--r--app/controllers/application_controller.rb33
-rw-r--r--app/controllers/autocomplete_controller.rb9
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb44
-rw-r--r--app/helpers/diff_helper.rb3
-rw-r--r--app/helpers/selects_helper.rb16
-rw-r--r--app/mailers/repository_check_mailer.rb14
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/oauth_access_token.rb19
-rw-r--r--app/models/project_services/bamboo_service.rb20
-rw-r--r--app/models/project_services/builds_email_service.rb10
-rw-r--r--app/models/project_services/teamcity_service.rb33
-rw-r--r--app/models/repository.rb9
-rw-r--r--app/services/issues/base_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb3
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb5
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml19
-rw-r--r--app/views/admin/logs/show.html.haml3
-rw-r--r--app/views/admin/projects/index.html.haml10
-rw-r--r--app/views/admin/projects/show.html.haml36
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/groups/milestones/new.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml4
-rw-r--r--app/views/projects/blob/diff.html.haml12
-rw-r--r--app/views/projects/diffs/_line.html.haml20
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml8
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/merge_requests/_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml5
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml4
-rw-r--r--app/views/projects/notes/discussions/_diff.html.haml8
-rw-r--r--app/views/projects/releases/edit.html.haml12
-rw-r--r--app/views/projects/tags/new.html.haml6
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/repository_check_mailer/notify.html.haml5
-rw-r--r--app/views/repository_check_mailer/notify.text.haml3
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml15
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml13
-rw-r--r--app/workers/admin_email_worker.rb12
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb63
-rw-r--r--app/workers/repository_check/clear_worker.rb17
-rw-r--r--app/workers/repository_check/single_repository_worker.rb36
-rwxr-xr-xbin/setup2
-rw-r--r--config/gitlab.yml.example7
-rw-r--r--config/initializers/1_settings.rb7
-rw-r--r--config/initializers/session_store.rb2
-rw-r--r--config/initializers/sidekiq.rb6
-rw-r--r--config/routes.rb6
-rw-r--r--db/migrate/20160315135439_project_add_repository_check.rb8
-rw-r--r--db/migrate/20160328115649_migrate_new_notification_setting.rb2
-rw-r--r--db/migrate/20160412140240_add_repository_checks_enabled_setting.rb5
-rw-r--r--db/schema.rb6
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/repository_checks.md43
-rw-r--r--doc/api/issues.md110
-rw-r--r--doc/api/merge_requests.md148
-rw-r--r--doc/api/notes.md1
-rw-r--r--doc/ci/runners/README.md2
-rw-r--r--doc/ci/yaml/README.md73
-rw-r--r--doc/development/rake_tasks.md2
-rw-r--r--doc/integration/shibboleth.md47
-rw-r--r--doc/monitoring/performance/grafana_configuration.md48
-rw-r--r--doc/update/8.5-to-8.6.md2
-rw-r--r--doc/workflow/web_editor.md2
-rw-r--r--features/steps/project/issues/issues.rb4
-rw-r--r--features/steps/project/labels.rb2
-rw-r--r--features/steps/project/merge_requests.rb4
-rw-r--r--features/steps/shared/diff_note.rb2
-rw-r--r--features/steps/shared/issuable.rb7
-rw-r--r--lib/api/issues.rb43
-rw-r--r--lib/api/merge_requests.rb36
-rw-r--r--lib/api/notes.rb5
-rw-r--r--lib/award_emoji.rb16
-rw-r--r--lib/gitlab/backend/shell.rb40
-rw-r--r--lib/gitlab/current_settings.rb3
-rw-r--r--lib/gitlab/exclusive_lease.rb5
-rw-r--r--lib/gitlab/gon_helper.rb17
-rw-r--r--lib/gitlab/note_data_builder.rb3
-rw-r--r--lib/gitlab/redis.rb2
-rw-r--r--lib/gitlab/repository_check_logger.rb7
-rw-r--r--lib/gitlab/url_builder.rb74
-rw-r--r--lib/support/nginx/gitlab_ci29
-rw-r--r--lib/tasks/gitlab/setup.rake2
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb24
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb12
-rw-r--r--spec/factories/commits.rb12
-rw-r--r--spec/factories/oauth_access_tokens.rb22
-rw-r--r--spec/factories/oauth_applications.rb9
-rw-r--r--spec/factories/users.rb2
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb43
-rw-r--r--spec/features/issues_spec.rb16
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb25
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb39
-rw-r--r--spec/features/search_spec.rb4
-rw-r--r--spec/javascripts/notes_spec.js.coffee1
-rw-r--r--spec/lib/award_emoji_spec.rb7
-rw-r--r--spec/lib/gitlab/note_data_builder_spec.rb3
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb143
-rw-r--r--spec/mailers/repository_check_mailer_spec.rb21
-rw-r--r--spec/models/issue_spec.rb17
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb262
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb37
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb251
-rw-r--r--spec/models/repository_spec.rb12
-rw-r--r--spec/requests/api/issues_spec.rb63
-rw-r--r--spec/requests/api/merge_requests_spec.rb42
-rw-r--r--spec/requests/api/notes_spec.rb13
-rw-r--r--spec/services/delete_tag_service_spec.rb11
-rw-r--r--spec/services/projects/transfer_service_spec.rb23
-rw-r--r--spec/workers/post_receive_spec.rb43
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb39
-rw-r--r--spec/workers/repository_check/clear_worker_spec.rb17
156 files changed, 2395 insertions, 663 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 42f73259fee..8c0e0f2f0d3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,31 +2,41 @@ 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
@@ -38,8 +48,10 @@ v 8.7.0 (unreleased)
- 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.)
+ - Allow issues and merge requests to be assigned to the author !2765
- 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)
@@ -55,6 +67,12 @@ v 8.7.0 (unreleased)
- 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
+ - Delete tags using Rugged for performance reasons (Robert Schilling)
+ - 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)
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 70fd6f50e9c..0b9110d35fa 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -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'
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index b502131a99d..6eb8d27ee2b 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -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
diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee
new file mode 100644
index 00000000000..d540cc4dc46
--- /dev/null
+++ b/app/assets/javascripts/gl_form.js.coffee
@@ -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'
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index 946d83b7bdd..c7d74a12f99 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -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)
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 90385621879..bc80980acb7 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -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) ->
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index ef0b534a709..1ab6e5114bc 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -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) ->
@@ -152,12 +154,38 @@ class @MergeRequestTabs
@_get
url: "#{source}.json" + @_location.search
success: (data) =>
- document.querySelector("div#diffs").innerHTML = data.html
+ $('#diffs').html data.html
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
- $('div#diffs .js-syntax-highlight').syntaxHighlight()
+ $('#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
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index a67890200dd..fa91baa07c0 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -283,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()
@@ -317,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
@@ -375,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)
-
- 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
+ new GLForm form
- if isNewForm
- disableButtonIfEmptyField textarea, form.find(".js-comment-button")
+ form.find(".js-note-text").focus()
###
Called in response to clicking the edit note link
@@ -559,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)
@@ -570,7 +527,6 @@ class @Notes
# only remove the form
form.remove()
-
cancelDiscussionForm: (e) =>
e.preventDefault()
form = $(e.target).closest(".js-discussion-note-form")
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index e4b7a3172ec..1a430f3aa47 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -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)
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index eee9b6e690e..a7e934936e9 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -12,6 +12,7 @@ class @UsersSelect
showNullUser = $dropdown.data('null-user')
showAnyUser = $dropdown.data('any-user')
firstUser = $dropdown.data('first-user')
+ @authorId = $dropdown.data('author-id')
selectedId = $dropdown.data('selected')
defaultLabel = $dropdown.data('default-label')
issueURL = $dropdown.data('issueUpdate')
@@ -207,6 +208,7 @@ class @UsersSelect
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
@showCurrentUser = $(select).data('current-user')
+ @authorId = $(select).data('author-id')
showNullUser = $(select).data('null-user')
showAnyUser = $(select).data('any-user')
showEmailUser = $(select).data('email-user')
@@ -312,6 +314,7 @@ class @UsersSelect
project_id: @projectId
group_id: @groupId
current_user: @showCurrentUser
+ author_id: @authorId
dataType: "json"
).done (users) ->
callback(users)
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 5ae0520fd7b..f4d35c4b4b1 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -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;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index c8f86d60e3b..0f32d36d59c 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 7b2aada5a0d..0a5b4b8834c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 0b6be86ce6a..f910cf61817 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -150,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;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 47673944896..77a73dc379b 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -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);
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 806401c21ae..28253d4ccb4 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -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);
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 6a809d4dfd2..c62bd021aef 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -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);
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index c482a1258f7..524cfaf90c3 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -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);
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 28331f59754..1ff6ad75e07 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index d0855f66911..183f22a1b24 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -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);
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 8b6f37f21b5..6bd90a23620 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -173,12 +173,6 @@
}
}
- .subscribe-button {
- span {
- margin-top: 0;
- }
- }
-
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
display: none;
@@ -322,3 +316,9 @@
color: #8c8c8c;
}
}
+
+.issuable-form-padding-top {
+ @media (min-width: $screen-sm-min) {
+ padding-top: 7px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 3e0a3140be7..e179bdf0048 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -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;
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f4da17fadaa..07c707e7b77 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -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,
@@ -72,6 +73,17 @@
}
}
+ &.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;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e421a31549a..ce44f5aa13b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -276,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 {
@@ -291,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;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index f010436bd36..b4a28b8dd3f 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -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: []
)
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index c6b3105544a..87986fdf8b1 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 97d53acde94..1c53b0b21a3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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]
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 81ba58ce49c..eb0abc80ab4 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -12,8 +12,15 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user] && current_user
- @users = [*@users, current_user].uniq
+ @users = [*@users, current_user]
end
+
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users = [author, *@users] if author
+ end
+
+ @users.uniq!
end
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index c1adc999567..ee4fcc4e360 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -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?
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index d1e4ac10f6c..c6bdd0602c1 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -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'
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index b88c080352b..a12549d6bcb 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -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)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 6d649e72f84..38214f04793 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -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
@@ -62,11 +60,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- @note = @project.notes.new(noteable: @issue)
- @notes = @issue.notes.nonawards.with_associations.fresh
+ @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
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index ff32e834499..6a3ec83b8c0 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -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?
" &nbsp;".html_safe
else
+ line[0] = ' ' if %w[new old].include?(line_type)
line
end
end
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 05386d790ca..4fc6de59a8b 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -6,12 +6,13 @@ module SelectsHelper
value = opts[:selected] || ''
placeholder = opts[:placeholder] || 'Search for a user'
- null_user = opts[:null_user] || false
- any_user = opts[:any_user] || false
- email_user = opts[:email_user] || false
- first_user = opts[:first_user] && current_user ? current_user.username : false
- current_user = opts[:current_user] || false
- project = opts[:project] || @project
+ null_user = opts[:null_user] || false
+ any_user = opts[:any_user] || false
+ email_user = opts[:email_user] || false
+ first_user = opts[:first_user] && current_user ? current_user.username : false
+ current_user = opts[:current_user] || false
+ author_id = opts[:author_id] || ''
+ project = opts[:project] || @project
html = {
class: css_class,
@@ -21,7 +22,8 @@ module SelectsHelper
any_user: any_user,
email_user: email_user,
first_user: first_user,
- current_user: current_user
+ current_user: current_user,
+ author_id: author_id
}
}
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
new file mode 100644
index 00000000000..2bff5b63cc4
--- /dev/null
+++ b/app/mailers/repository_check_mailer.rb
@@ -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
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 052cd874733..36f88154232 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -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
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 11ecfcace14..d1f07ccd55c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -154,7 +154,7 @@ class Commit
id: id,
message: safe_message,
timestamp: committed_date.xmlschema,
- url: commit_url,
+ url: Gitlab::UrlBuilder.build(self),
author: {
name: author_name,
email: author_email
@@ -168,10 +168,6 @@ class Commit
data
end
- def commit_url
- project.present? ? "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{id}" : ""
- end
-
# Discover issues should be closed when this commit is pushed to a project's
# default branch.
def closes_issues(current_user = self.committer)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index e064b0f8b95..3f188e04770 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -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)
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
new file mode 100644
index 00000000000..c78c7f4aa0e
--- /dev/null
+++ b/app/models/oauth_access_token.rb
@@ -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
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 9e7f642180e..060062aaf7a 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -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
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index f9f04838766..6ab6d7417b7 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -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
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b8e9416131a..8dceee5e2c5 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0b2289cfa39..308c590e3f8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -169,7 +169,12 @@ class Repository
def rm_tag(tag_name)
before_remove_tag
- gitlab_shell.rm_tag(path_with_namespace, tag_name)
+ begin
+ rugged.tags.delete(tag_name)
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
end
def branch_names
@@ -797,7 +802,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
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 770f32de944..772f5c5fffa 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -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
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index ac5b58db862..e6837a18696 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -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
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 6e9152e444e..fa34753c4fd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -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}"
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 2e734654466..79a27f4af7e 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -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
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 658b086496f..82a0e2fd1f5 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -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)
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index a8cca1a81cb..555aea554f0 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -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'
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index af9fdeb0734..4b475a4d8fa 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -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' : '') }
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index d39c0f44031..aa07afa0d62 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -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"
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index c638c32a654..73986d21bcf 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -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
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 55f4a6f287d..0aff79749ef 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -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
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 4290e0bf72e..7d9d27ae1fc 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -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
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 44339293095..3beb8ff7c0d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -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')
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index bddff5cdcbc..e1e35013968 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -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')
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index abcfca4cd11..38e62c81fed 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -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 }
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 9464c8dc996..107097ad963 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -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" ? "&nbsp;" : line.old_pos)
+ %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
+ - link_text = type == "new" ? "&nbsp;".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" ? "&nbsp;" : line.new_pos)
+ %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
+ - link_text = type == "old" ? "&nbsp;".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)
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index d7c49068745..81948513e43 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -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])
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 6d872cd0b21..76a4f41193c 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -210,6 +210,7 @@
%li Be careful. Changing the project's namespace can have unintended side effects.
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location.
+ %li Project visibility level will be changed to match namespace rules when transfering to a group.
.form-actions
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- else
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 33c48199ba5..7076f5db015 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
:javascript
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 4aa92d0b39e..7a8009f6da4 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -27,7 +27,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = issue.notes.user.count
+ - note_count = issue.notes.user.nonawards.count
- if note_count > 0
%li
= link_to issue_path(issue) + "#notes" do
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 6fa059cbe68..5fe5ddc0819 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -64,9 +64,11 @@
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
- .merge-requests
- = render 'merge_requests'
- = render 'related_branches'
+ #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)}
+ // This element is filled in using JavaScript.
+
+ #related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)}
+ // This element is filled in using JavaScript.
.content-block.content-block-small
= render 'new_branch'
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 097a65969a6..8bf544b8371 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -14,7 +14,7 @@
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
.subscription-status{data: {status: label_subscription_status(label)}}
- %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}}
+ %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } }
%span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 1e6724fc92b..88525f4036a 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 391193eed6c..e740fe8c84d 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -35,7 +35,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.count
+ - note_count = merge_request.mr_and_commit_notes.user.nonawards.count
- if note_count > 0
%li
= link_to merge_request_path(merge_request) + "#notes" do
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 9e59f7df71b..2f14a91e64f 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -10,7 +10,7 @@
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 1dd8f721f7e..07037a14f51 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -51,7 +51,7 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
+ %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index b2dae1c70ee..687222fa92f 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,6 +1,5 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
= form_errors(@milestone)
-
.row
.col-md-6
.form-group
@@ -11,7 +10,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...'
= render 'projects/notes/hints'
.clearfix
.error-alert
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 23e4f93eab5..c87a3fadf72 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -2,7 +2,7 @@
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.note-form-actions.clearfix
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index c446ecec2c3..d0ac380f216 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.error-alert
diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml
index 820e31ccd61..d46aab000c3 100644
--- a/app/views/projects/notes/discussions/_diff.html.haml
+++ b/app/views/projects/notes/discussions/_diff.html.haml
@@ -20,11 +20,9 @@
%td.new_line.diff-line-num= "..."
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num
- = raw(type == "new" ? "&nbsp;" : line.old_pos)
- %td.new_line.diff-line-num
- = raw(type == "old" ? "&nbsp;" : line.new_pos)
- %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text)
+ %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? "&nbsp;".html_safe : line.old_pos } }
+ %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? "&nbsp;".html_safe : line.new_pos } }
+ %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type)
- if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index c4a3f06ee06..6f0b32aa165 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -9,11 +9,11 @@
%strong #{@tag.name}
.prepend-top-default
- = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f|
+ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
- .error-alert
- .form-actions.prepend-top-default
- = f.submit 'Save changes', class: 'btn btn-save'
- = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
+ .error-alert
+ .form-actions.prepend-top-default
+ = f.submit 'Save changes', class: 'btn btn-save'
+ = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 77c7c4d23de..b40a6e5cb2d 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -10,7 +10,7 @@
New Tag
%hr
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do
.form-group
= label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
@@ -30,9 +30,9 @@
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'description form-control'
+ = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
- .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
+ .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 812876e2835..797a1a59e9f 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
= f.hidden_field :title, value: @page.title
@@ -11,7 +11,7 @@
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
= render 'projects/notes/hints'
.clearfix
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
new file mode 100644
index 00000000000..df16f503570
--- /dev/null
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{@message}.
+
+%p
+ = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
new file mode 100644
index 00000000000..02f3f80288a
--- /dev/null
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -0,0 +1,3 @@
+#{@message}.
+\
+View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index 9544e3d3e17..d9400b1d9fa 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -1,5 +1,5 @@
- project = note.project
-- note_url = Gitlab::UrlBuilder.new(:note).build(note.id)
+- note_url = Gitlab::UrlBuilder.build(note)
- noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row
%h5.note-search-caption.str-truncated
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 757a3812deb..aed2622a6da 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -29,7 +29,8 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
- classes: 'description form-control'
+ classes: 'note-textarea',
+ placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.clearfix
.error-alert
@@ -70,13 +71,13 @@
- if can? current_user, :admin_milestone, issuable.project
= link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank
.form-group
+ - has_labels = issuable.project.labels.any?
= f.label :label_ids, "Labels", class: 'control-label'
- .col-sm-10
- - if issuable.project.labels.any?
+ .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
+ - if has_labels
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
{ selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else
- .prepend-top-10
%span.light No labels yet.
&nbsp;
- if can? current_user, :admin_label, issuable.project
@@ -128,8 +129,6 @@
- else
.pull-right
- if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
- = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
- method: :delete, class: 'btn btn-grouped' do
- = icon('trash-o')
- Delete
+ = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+ method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 94affa4b59a..08bfd93f4e6 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -49,7 +49,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
- = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
+ = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone
.sidebar-collapsed-icon
@@ -128,7 +128,7 @@
.title.hide-collapsed
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
+ %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
@@ -152,4 +152,4 @@
new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription')
- new Sidebar(); \ No newline at end of file
+ new Sidebar();
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 868b2357003..b15e8ea73fe 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -4,15 +4,16 @@
%li
%span.label-row
- = link_to milestones_label_path(options) do
- - render_colored_label(label, tooltip: false)
- %span.prepend-left-10
+ %span.label-name
+ = link_to milestones_label_path(options) do
+ - render_colored_label(label, tooltip: false)
+ %span.prepend-description-left
= markdown(label.description, pipeline: :single_line)
- .pull-right
- %strong.issues-count
+ .pull-info-right
+ %span.append-right-20
= link_to milestones_label_path(options.merge(state: 'opened')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
- %strong.issues-count
+ %span.append-right-20
= link_to milestones_label_path(options.merge(state: 'closed')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
new file mode 100644
index 00000000000..667fff031dd
--- /dev/null
+++ b/app/workers/admin_email_worker.rb
@@ -0,0 +1,12 @@
+class AdminEmailWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false # this job auto-repeats via sidekiq-cron
+
+ def perform
+ repository_check_failed_count = Project.where(last_repository_check_failed: true).count
+ return if repository_check_failed_count.zero?
+
+ RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 3cc232ef1ae..9e1215b21a6 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -40,7 +40,7 @@ class PostReceive
if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
- else
+ elsif Gitlab::Git.branch_ref?(ref)
GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
end
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
new file mode 100644
index 00000000000..44b3145d50f
--- /dev/null
+++ b/app/workers/repository_check/batch_worker.rb
@@ -0,0 +1,63 @@
+module RepositoryCheck
+ class BatchWorker
+ include Sidekiq::Worker
+
+ RUN_TIME = 3600
+
+ sidekiq_options retry: false
+
+ def perform
+ start = Time.now
+
+ # This loop will break after a little more than one hour ('a little
+ # more' because `git fsck` may take a few minutes), or if it runs out of
+ # projects to check. By default sidekiq-cron will start a new
+ # RepositoryCheckWorker each hour so that as long as there are repositories to
+ # check, only one (or two) will be checked at a time.
+ project_ids.each do |project_id|
+ break if Time.now - start >= RUN_TIME
+ break unless current_settings.repository_checks_enabled
+
+ next unless try_obtain_lease(project_id)
+
+ SingleRepositoryWorker.new.perform(project_id)
+ end
+ end
+
+ private
+
+ # Project.find_each does not support WHERE clauses and
+ # Project.find_in_batches does not support ordering. So we just build an
+ # array of ID's. This is OK because we do it only once an hour, because
+ # getting ID's from Postgres is not terribly slow, and because no user
+ # has to sit and wait for this query to finish.
+ def project_ids
+ limit = 10_000
+ never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit).
+ pluck(:id)
+ old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago).
+ reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
+ never_checked_projects + old_check_projects
+ end
+
+ def try_obtain_lease(id)
+ # Use a 24-hour timeout because on servers/projects where 'git fsck' is
+ # super slow we definitely do not want to run it twice in parallel.
+ Gitlab::ExclusiveLease.new(
+ "project_repository_check:#{id}",
+ timeout: 24.hours
+ ).try_obtain
+ end
+
+ def current_settings
+ # No caching of the settings! If we cache them and an admin disables
+ # this feature, an active RepositoryCheckWorker would keep going for up
+ # to 1 hour after the feature was disabled.
+ if Rails.env.test?
+ Gitlab::CurrentSettings.fake_application_settings
+ else
+ ApplicationSetting.current
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
new file mode 100644
index 00000000000..b7202ddff34
--- /dev/null
+++ b/app/workers/repository_check/clear_worker.rb
@@ -0,0 +1,17 @@
+module RepositoryCheck
+ class ClearWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false
+
+ def perform
+ # Do small batched updates because these updates will be slow and locking
+ Project.select(:id).find_in_batches(batch_size: 100) do |batch|
+ Project.where(id: batch.map(&:id)).update_all(
+ last_repository_check_failed: nil,
+ last_repository_check_at: nil,
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
new file mode 100644
index 00000000000..e54ae86d06c
--- /dev/null
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -0,0 +1,36 @@
+module RepositoryCheck
+ class SingleRepositoryWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false
+
+ def perform(project_id)
+ project = Project.find(project_id)
+ project.update_columns(
+ last_repository_check_failed: !check(project),
+ last_repository_check_at: Time.now,
+ )
+ end
+
+ private
+
+ def check(project)
+ # Use 'map do', not 'all? do', to prevent short-circuiting
+ [project.repository, project.wiki.repository].map do |repository|
+ git_fsck(repository.path_to_repo)
+ end.all?
+ end
+
+ def git_fsck(path)
+ cmd = %W(nice git --git-dir=#{path} fsck)
+ output, status = Gitlab::Popen.popen(cmd)
+
+ if status.zero?
+ true
+ else
+ Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}")
+ false
+ end
+ end
+ end
+end
diff --git a/bin/setup b/bin/setup
index acdb2c1389c..6cb2d7f1e3a 100755
--- a/bin/setup
+++ b/bin/setup
@@ -18,7 +18,7 @@ Dir.chdir APP_ROOT do
# end
puts "\n== Preparing database =="
- system "bin/rake db:setup"
+ system "bin/rake db:reset"
puts "\n== Removing old logs and tempfiles =="
system "rm -f log/*"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index b28fc5c8e01..d9c15f81404 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -164,6 +164,13 @@ production: &base
# Flag stuck CI builds as failed
stuck_ci_builds_worker:
cron: "0 0 * * *"
+ # Periodically run 'git fsck' on all repositories. If started more than
+ # once per hour you will have concurrent 'git fsck' jobs.
+ repository_check_worker:
+ cron: "20 * * * *"
+ # Send admin emails once a day
+ admin_email_worker:
+ cron: "0 0 * * *"
# Remove outdated repository archives
repository_archive_cache_worker:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 287f99c724d..10c25044b75 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -241,11 +241,16 @@ Settings['cron_jobs'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
+Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *'
+Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker'
+Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *'
+Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
-
#
# GitLab Shell
#
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 70285255877..88cb859871c 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -14,7 +14,7 @@ if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
redis_config = Gitlab::Redis.redis_store_options
- redis_config[:namespace] = 'session:gitlab'
+ redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 9182d929809..f1eec674888 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,9 +1,7 @@
-SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
-
Sidekiq.configure_server do |config|
config.redis = {
url: Gitlab::Redis.url,
- namespace: SIDEKIQ_REDIS_NAMESPACE
+ namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
}
config.server_middleware do |chain|
@@ -30,6 +28,6 @@ end
Sidekiq.configure_client do |config|
config.redis = {
url: Gitlab::Redis.url,
- namespace: SIDEKIQ_REDIS_NAMESPACE
+ namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
}
end
diff --git a/config/routes.rb b/config/routes.rb
index 48601b7567b..46a25262844 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -264,6 +264,7 @@ Rails.application.routes.draw do
member do
put :transfer
+ post :repository_check
end
resources :runner_projects
@@ -281,6 +282,7 @@ Rails.application.routes.draw do
resource :application_settings, only: [:show, :update] do
resources :services
put :reset_runners_token
+ put :clear_repository_check_states
end
resources :labels
@@ -326,7 +328,7 @@ Rails.application.routes.draw do
end
end
resource :preferences, only: [:show, :update]
- resources :keys, except: [:new]
+ resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resource :two_factor_auth, only: [:new, :create, :destroy] do
@@ -701,6 +703,8 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
+ get :referenced_merge_requests
+ get :related_branches
end
collection do
post :bulk_update
diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb
new file mode 100644
index 00000000000..8687d5d6296
--- /dev/null
+++ b/db/migrate/20160315135439_project_add_repository_check.rb
@@ -0,0 +1,8 @@
+class ProjectAddRepositoryCheck < ActiveRecord::Migration
+ def change
+ add_column :projects, :last_repository_check_failed, :boolean
+ add_index :projects, :last_repository_check_failed
+
+ add_column :projects, :last_repository_check_at, :datetime
+ end
+end
diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb
index 0a110869027..3c81b2c37bf 100644
--- a/db/migrate/20160328115649_migrate_new_notification_setting.rb
+++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb
@@ -7,7 +7,7 @@
#
class MigrateNewNotificationSetting < ActiveRecord::Migration
def up
- timestamp = Time.now
+ timestamp = Time.now.strftime('%F %T')
execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL"
end
diff --git a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb
new file mode 100644
index 00000000000..ebfa4bcbc7b
--- /dev/null
+++ b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb
@@ -0,0 +1,5 @@
+class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :repository_checks_enabled, :boolean, default: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 44482de467e..d36e2b235e5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160331223143) do
+ActiveRecord::Schema.define(version: 20160412140240) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -77,6 +77,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do
t.string "akismet_api_key"
t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility"
+ t.boolean "repository_checks_enabled", default: true
end
create_table "audit_events", force: :cascade do |t|
@@ -743,6 +744,8 @@ ActiveRecord::Schema.define(version: 20160331223143) do
t.boolean "public_builds", default: true, null: false
t.string "main_language"
t.integer "pushes_since_gc", default: 0
+ t.boolean "last_repository_check_failed"
+ t.datetime "last_repository_check_at"
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
@@ -752,6 +755,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
diff --git a/doc/README.md b/doc/README.md
index d2660930653..e6ac4794827 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -31,6 +31,7 @@
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
+- [Repository checks](administration/repository_checks.md) Periodic Git repository checks
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
new file mode 100644
index 00000000000..61bf8ce6161
--- /dev/null
+++ b/doc/administration/repository_checks.md
@@ -0,0 +1,43 @@
+# Repository checks
+
+>**Note:**
+This feature was [introduced][ce-3232] in GitLab 8.7.
+
+Git has a built-in mechanism, [git fsck][git-fsck], to verify the
+integrity of all data commited to a repository. GitLab administrators
+can trigger such a check for a project via the project page under the
+admin panel. The checks run asynchronously so it may take a few minutes
+before the check result is visible on the project admin page. If the
+checks failed you can see their output on the admin log page under
+'repocheck.log'.
+
+## Periodic checks
+
+GitLab periodically runs a repository check on all project repositories and
+wiki repositories in order to detect data corruption problems. A
+project will be checked no more than once per week. If any projects
+fail their repository checks all GitLab administrators will receive an email
+notification of the situation. This notification is sent out no more
+than once a day.
+
+## Disabling periodic checks
+
+You can disable the periodic checks on the 'Settings' page of the admin
+panel.
+
+## What to do if a check failed
+
+If the repository check fails for some repository you should look up the error
+in repocheck.log (in the admin panel or on disk; see
+`/var/log/gitlab/gitlab-rails` for Omnibus installations or
+`/home/git/gitlab/log` for installations from source). Once you have
+resolved the issue use the admin panel to trigger a new repository check on
+the project. This will clear the 'check failed' state.
+
+If for some reason the periodic repository check caused a lot of false
+alarms you can choose to clear ALL repository check states from the
+'Settings' page of the admin panel.
+
+---
+[ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck"
+[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" \ No newline at end of file
diff --git a/doc/api/issues.md b/doc/api/issues.md
index f09847aef95..3e78149f442 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -298,6 +298,7 @@ PUT /projects/:id/issues/:issue_id
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
+| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
```bash
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
@@ -406,6 +407,115 @@ Example response:
}
```
+## Subscribe to an issue
+
+Subscribes the authenticated user to an issue to receive notifications. If the
+operation is successful, status code `201` together with the updated issue is
+returned. If the user is already subscribed to the issue, the status code `304`
+is returned. If the project or issue is not found, status code `404` is
+returned.
+
+```
+POST /projects/:id/issues/:issue_id/subscription
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
+```
+
+Example response:
+
+```json
+{
+ "id": 92,
+ "iid": 11,
+ "project_id": 5,
+ "title": "Sit voluptas tempora quisquam aut doloribus et.",
+ "description": "Repellat voluptas quibusdam voluptatem exercitationem.",
+ "state": "opened",
+ "created_at": "2016-04-05T21:41:45.652Z",
+ "updated_at": "2016-04-07T12:20:17.596Z",
+ "labels": [],
+ "milestone": null,
+ "assignee": {
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/axel.block"
+ },
+ "author": {
+ "name": "Kris Steuber",
+ "username": "solon.cremin",
+ "id": 10,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/solon.cremin"
+ }
+}
+```
+
+## Unsubscribe from an issue
+
+Unsubscribes the authenticated user from the issue to not receive notifications
+from it. If the operation is successful, status code `200` together with the
+updated issue is returned. If the user is not subscribed to the issue, the
+status code `304` is returned. If the project or issue is not found, status code
+`404` is returned.
+
+```
+DELETE /projects/:id/issues/:issue_id/subscription
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
+```
+
+Example response:
+
+```json
+{
+ "id": 93,
+ "iid": 12,
+ "project_id": 5,
+ "title": "Incidunt et rerum ea expedita iure quibusdam.",
+ "description": "Et cumque architecto sed aut ipsam.",
+ "state": "opened",
+ "created_at": "2016-04-05T21:41:45.217Z",
+ "updated_at": "2016-04-07T13:02:37.905Z",
+ "labels": [],
+ "milestone": null,
+ "assignee": {
+ "name": "Edwardo Grady",
+ "username": "keyon",
+ "id": 21,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/keyon"
+ },
+ "author": {
+ "name": "Vivian Hermann",
+ "username": "orville",
+ "id": 11,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
+ "web_url": "http://lgitlab.example.com/u/orville"
+ },
+ "subscribed": false
+}
+```
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 20db73ea6c0..2057f9d77aa 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -606,3 +606,151 @@ Example response:
},
]
```
+
+## Subscribe to a merge request
+
+Subscribes the authenticated user to a merge request to receive notification. If
+the operation is successful, status code `201` together with the updated merge
+request is returned. If the user is already subscribed to the merge request, the
+status code `304` is returned. If the project or merge request is not found,
+status code `404` is returned.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/subscription
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+```
+
+Example response:
+
+```json
+{
+ "id": 17,
+ "iid": 1,
+ "project_id": 5,
+ "title": "Et et sequi est impedit nulla ut rem et voluptatem.",
+ "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.",
+ "state": "opened",
+ "created_at": "2016-04-05T21:42:23.233Z",
+ "updated_at": "2016-04-05T22:11:52.900Z",
+ "target_branch": "ui-dev-kit",
+ "source_branch": "version-1-9",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Eileen Skiles",
+ "username": "leila",
+ "id": 19,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/leila"
+ },
+ "assignee": {
+ "name": "Celine Wehner",
+ "username": "carli",
+ "id": 16,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/carli"
+ },
+ "source_project_id": 5,
+ "target_project_id": 5,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 7,
+ "iid": 1,
+ "project_id": 5,
+ "title": "v2.0",
+ "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.",
+ "state": "closed",
+ "created_at": "2016-04-05T21:41:40.905Z",
+ "updated_at": "2016-04-05T21:41:40.905Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true
+}
+```
+
+## Unsubscribe from a merge request
+
+Unsubscribes the authenticated user from a merge request to not receive
+notifications from that merge request. If the operation is successful, status
+code `200` together with the updated merge request is returned. If the user is
+not subscribed to the merge request, the status code `304` is returned. If the
+project or merge request is not found, status code `404` is returned.
+
+```
+DELETE /projects/:id/merge_requests/:merge_request_id/subscription
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+```
+
+Example response:
+
+```json
+{
+ "id": 17,
+ "iid": 1,
+ "project_id": 5,
+ "title": "Et et sequi est impedit nulla ut rem et voluptatem.",
+ "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.",
+ "state": "opened",
+ "created_at": "2016-04-05T21:42:23.233Z",
+ "updated_at": "2016-04-05T22:11:52.900Z",
+ "target_branch": "ui-dev-kit",
+ "source_branch": "version-1-9",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Eileen Skiles",
+ "username": "leila",
+ "id": 19,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/leila"
+ },
+ "assignee": {
+ "name": "Celine Wehner",
+ "username": "carli",
+ "id": 16,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/carli"
+ },
+ "source_project_id": 5,
+ "target_project_id": 5,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 7,
+ "iid": 1,
+ "project_id": 5,
+ "title": "v2.0",
+ "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.",
+ "state": "closed",
+ "created_at": "2016-04-05T21:41:40.905Z",
+ "updated_at": "2016-04-05T21:41:40.905Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": false
+}
+```
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 2e0936f11b5..7aa1c2155bf 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -89,6 +89,7 @@ Parameters:
- `id` (required) - The ID of a project
- `issue_id` (required) - The ID of an issue
- `body` (required) - The content of a note
+- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
### Modify existing issue note
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 295d953db11..c7df0713a3d 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -19,7 +19,7 @@ many projects, you can have a single or a small number of runners that handle
multiple projects. This makes it easier to maintain and update runners.
**Specific runners** are useful for jobs that have special requirements or for
-projects with a very demand. If a job has certain requirements, you can set
+projects with a specific demand. If a job has certain requirements, you can set
up the specific runner with this in mind, while not having to do this for all
runners. For example, if you want to deploy a certain project, you can setup
a specific runner to have the right credentials for this.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7da9b31e30d..abb6e97e5e6 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1,5 +1,47 @@
# Configuration of your builds with .gitlab-ci.yml
+This document describes the usage of `.gitlab-ci.yml`, the file that is used by
+GitLab Runner to manage your project's builds.
+
+If you want a quick introduction to GitLab CI, follow our
+[quick start guide](../quick_start/README.md).
+
+---
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [.gitlab-ci.yml](#gitlab-ci-yml)
+ - [image and services](#image-and-services)
+ - [before_script](#before_script)
+ - [stages](#stages)
+ - [types](#types)
+ - [variables](#variables)
+ - [cache](#cache)
+ - [cache:key](#cache-key)
+- [Jobs](#jobs)
+ - [script](#script)
+ - [stage](#stage)
+ - [only and except](#only-and-except)
+ - [tags](#tags)
+ - [when](#when)
+ - [artifacts](#artifacts)
+ - [artifacts:name](#artifacts-name)
+ - [dependencies](#dependencies)
+- [Hidden jobs](#hidden-jobs)
+- [Special YAML features](#special-yaml-features)
+ - [Anchors](#anchors)
+- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
+- [Skipping builds](#skipping-builds)
+- [Examples](#examples)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+---
+
+## .gitlab-ci.yml
+
From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML)
file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root
of your repository and contains definitions of how your project should be built.
@@ -23,12 +65,10 @@ Of course a command can execute code directly (`./configure;make;make install`)
or run a script (`test.sh`) in the repository.
Jobs are used to create builds, which are then picked up by
-[runners](../runners/README.md) and executed within the environment of the
-runner. What is important, is that each job is run independently from each
+[Runners](../runners/README.md) and executed within the environment of the
+Runner. What is important, is that each job is run independently from each
other.
-## .gitlab-ci.yml
-
The YAML syntax allows for using more complex job specifications than in the
above example:
@@ -71,7 +111,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
This allows to specify a custom Docker image and a list of services that can be
used for time of the build. The configuration of this feature is covered in
-separate document: [Use Docker](../docker/README.md).
+[a separate document](../docker/README.md).
### before_script
@@ -86,7 +126,8 @@ The specification of `stages` allows for having flexible multi stage pipelines.
The ordering of elements in `stages` defines the ordering of builds' execution:
1. Builds of the same stage are run in parallel.
-1. Builds of next stage are run after success.
+1. Builds of the next stage are run after the jobs from the previous stage
+ complete successfully.
Let's consider the following example, which defines 3 stages:
@@ -98,9 +139,9 @@ stages:
```
1. First all jobs of `build` are executed in parallel.
-1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel.
-1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel.
-1. If all jobs of `deploy` succeeds, the commit is marked as `success`.
+1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
+1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
+1. If all jobs of `deploy` succeed, the commit is marked as `success`.
1. If any of the previous jobs fails, the commit is marked as `failed` and no
jobs of further stage are executed.
@@ -278,14 +319,14 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
-| script | yes | Defines a shell script which is executed by runner |
+| script | yes | Defines a shell script which is executed by Runner |
| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created |
-| tags | no | Defines a list of tags which are used to select runner |
+| tags | no | Defines a list of tags which are used to select Runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
@@ -294,7 +335,7 @@ job_name:
### script
-`script` is a shell script which is executed by the runner. For example:
+`script` is a shell script which is executed by the Runner. For example:
```yaml
job:
@@ -375,13 +416,13 @@ except master.
### tags
-`tags` is used to select specific runners from the list of all runners that are
+`tags` is used to select specific Runners from the list of all Runners that are
allowed to run this project.
-During the registration of a runner, you can specify the runner's tags, for
+During the registration of a Runner, you can specify the Runner's tags, for
example `ruby`, `postgres`, `development`.
-`tags` allow you to run builds with runners that have the specified tags
+`tags` allow you to run builds with Runners that have the specified tags
assigned to them:
```yaml
@@ -391,7 +432,7 @@ job:
- postgres
```
-The specification above, will make sure that `job` is built by a runner that
+The specification above, will make sure that `job` is built by a Runner that
has both `ruby` AND `postgres` tags defined.
### when
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 9f3fd69fc4e..6d04b9590e6 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -9,7 +9,7 @@ bundle exec rake setup
```
The `setup` task is a alias for `gitlab:setup`.
-This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
+This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing.
## Run tests
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index a0be3dd4e5c..b6b2d4e5e88 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -76,3 +76,50 @@ sudo gitlab-ctl reconfigure
```
On the sign in page there should now be a "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in.
+
+## Apache 2.4 / GitLab 8.6 update
+The order of the first 2 Location directives is important. If they are reversed,
+you will not get a shibboleth session!
+
+```
+ <Location />
+ Require all granted
+ ProxyPassReverse http://127.0.0.1:8181
+ ProxyPassReverse http://YOUR_SERVER_FQDN/
+ </Location>
+
+ <Location /users/auth/shibboleth/callback>
+ AuthType shibboleth
+ ShibRequestSetting requireSession 1
+ ShibUseHeaders On
+ Require shib-session
+ </Location>
+
+ Alias /shibboleth-sp /usr/share/shibboleth
+
+ <Location /shibboleth-sp>
+ Require all granted
+ </Location>
+
+ <Location /Shibboleth.sso>
+ SetHandler shib
+ </Location>
+
+ RewriteEngine on
+
+ #Don't escape encoded characters in api requests
+ RewriteCond %{REQUEST_URI} ^/api/v3/.*
+ RewriteCond %{REQUEST_URI} !/Shibboleth.sso
+ RewriteCond %{REQUEST_URI} !/shibboleth-sp
+ RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE]
+
+ #Forward all requests to gitlab-workhorse except existing files
+ RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f [OR]
+ RewriteCond %{REQUEST_URI} ^/uploads/.*
+ RewriteCond %{REQUEST_URI} !/Shibboleth.sso
+ RewriteCond %{REQUEST_URI} !/shibboleth-sp
+ RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA]
+
+ RequestHeader set X_FORWARDED_PROTO 'https'
+ RequestHeader set X-Forwarded-Ssl on
+``` \ No newline at end of file
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
index 10ef1009818..a79c8d48d3b 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -61,24 +61,32 @@ contents below and paste it in to the interactive session:
```
CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT
CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1
-CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END
-CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END
-CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
-CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM rails_transactions GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM sidekiq_transactions GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM rails_method_calls GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM sidekiq_method_calls GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM rails_method_calls GROUP BY time(1m), method END;
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM sidekiq_method_calls GROUP BY time(1m), method END;
+CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM rails_memory_usage GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM sidekiq_memory_usage GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM sidekiq_file_descriptors GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM rails_file_descriptors GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM rails_gc_statistics GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM rails_gc_statistics GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM rails_gc_statistics GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_internal_allowed_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_allowed_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_internal_allowed_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_internal_allowed_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_authorized_keys_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_internal_authorized_keys_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.rails_transaction_timings FROM rails_transactions GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.sidekiq_transaction_timings FROM sidekiq_transactions GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_transaction_counts FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END;
+CREATE CONTINUOUS QUERY grape_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.grape_transaction_timings FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END;
```
## Import Dashboards
@@ -106,6 +114,10 @@ navigate away.
Repeat this process for each dashboard you wish to import.
+Alternatively you can automatically import all the dashboards into your Grafana
+instance. See the README of the [Grafana dashboards][grafana-dashboards]
+repository for more information on this process.
+
[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
---
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index b9abcbd2c12..6267f14eba4 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-6-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v2.6.11
+sudo -u git -H git checkout v2.6.12
```
### 5. Update gitlab-workhorse
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index 5685a9d89dd..1832567a34c 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -85,7 +85,7 @@ Once you click it, a new branch will be created that diverges from the default
branch of your project, by default `master`. The branch name will be based on
the title of the issue and as suffix it will have its ID. Thus, the example
screenshot above will yield a branch named
-`et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum-2`.
+`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
After the branch is created, you can edit files in the repository to fix
the issue. When a merge request is created based on the newly created branch,
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index aff5ca676be..fc12843ea5c 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -20,11 +20,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I should see that I am subscribed' do
- expect(find('.subscribe-button span')).to have_content 'Unsubscribe'
+ expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
- expect(find('.subscribe-button span')).to have_content 'Subscribe'
+ expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click link "Closed"' do
diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb
index 17944527e3a..5bb02189021 100644
--- a/features/steps/project/labels.rb
+++ b/features/steps/project/labels.rb
@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
private
def subscribe_button
- first('.subscribe-button span')
+ first('.label-subscribe-button span')
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index f0af0d097fa..4f883fe7c27 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -77,11 +77,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see that I am subscribed' do
- expect(find('.subscribe-button span')).to have_content 'Unsubscribe'
+ expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
- expect(find('.subscribe-button span')).to have_content 'Subscribe'
+ expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click button "Unsubscribe"' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 1448c3f44cc..e846c52d474 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -227,7 +227,7 @@ module SharedDiffNote
end
def click_diff_line(code)
- find("button[data-line-code='#{code}']").click
+ find("button[data-line-code='#{code}']").trigger('click')
end
def click_parallel_diff_line(code, line_type)
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index b6d70a26c21..24b3fb6eacb 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -71,13 +71,16 @@ module SharedIssuable
step 'I should not see any related merge requests' do
page.within '.issue-details' do
- expect(page).not_to have_content('.merge-requests')
+ expect(page).not_to have_content('#merge-requests .merge-requests-title')
end
end
step 'I should see the "Enterprise fix" related merge request' do
- page.within '.merge-requests' do
+ page.within '#merge-requests .merge-requests-title' do
expect(page).to have_content('1 Related Merge Request')
+ end
+
+ page.within '#merge-requests ul' do
expect(page).to have_content('Enterprise fix')
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 850e99981ff..8aa08fd5acc 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -117,7 +117,7 @@ module API
# assignee_id (optional) - The ID of a user to assign issue
# milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue
- # created_at (optional) - The date
+ # created_at (optional) - Date time string, ISO 8601 formatted
# Example Request:
# POST /projects/:id/issues
post ":id/issues" do
@@ -166,12 +166,15 @@ module API
# milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue
# state_event (optional) - The state event of an issue (close|reopen)
+ # updated_at (optional) - Date time string, ISO 8601 formatted
# Example Request:
# PUT /projects/:id/issues/:issue_id
put ":id/issues/:issue_id" do
issue = user_project.issues.find(params[:issue_id])
authorize! :update_issue, issue
- attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event]
+ keys = [:title, :description, :assignee_id, :milestone_id, :state_event]
+ keys << :updated_at if current_user.admin? || user_project.owner == current_user
+ attrs = attributes_for_keys(keys)
# Validate label names in advance
if (errors = validate_label_params(params)).any?
@@ -231,6 +234,42 @@ module API
authorize!(:destroy_issue, issue)
issue.destroy
end
+
+ # Subscribes to a project issue
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # issue_id (required) - The ID of a project issue
+ # Example Request:
+ # POST /projects/:id/issues/:issue_id/subscription
+ post ':id/issues/:issue_id/subscription' do
+ issue = user_project.issues.find(params[:issue_id])
+
+ if issue.subscribed?(current_user)
+ not_modified!
+ else
+ issue.toggle_subscription(current_user)
+ present issue, with: Entities::Issue, current_user: current_user
+ end
+ end
+
+ # Unsubscribes from a project issue
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # issue_id (required) - The ID of a project issue
+ # Example Request:
+ # DELETE /projects/:id/issues/:issue_id/subscription
+ delete ':id/issues/:issue_id/subscription' do
+ issue = user_project.issues.find(params[:issue_id])
+
+ if issue.subscribed?(current_user)
+ issue.unsubscribe(current_user)
+ present issue, with: Entities::Issue, current_user: current_user
+ else
+ not_modified!
+ end
+ end
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 4e7de8867b4..7e78609ecb9 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -327,6 +327,42 @@ module API
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: Entities::Issue, current_user: current_user
end
+
+ # Subscribes to a merge request
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of a merge request
+ # Example Request:
+ # POST /projects/:id/issues/:merge_request_id/subscription
+ post "#{path}/subscription" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ if merge_request.subscribed?(current_user)
+ not_modified!
+ else
+ merge_request.toggle_subscription(current_user)
+ present merge_request, with: Entities::MergeRequest, current_user: current_user
+ end
+ end
+
+ # Unsubscribes from a merge request
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of a merge request
+ # Example Request:
+ # DELETE /projects/:id/merge_requests/:merge_request_id/subscription
+ delete "#{path}/subscription" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ if merge_request.subscribed?(current_user)
+ merge_request.unsubscribe(current_user)
+ present merge_request, with: Entities::MergeRequest, current_user: current_user
+ else
+ not_modified!
+ end
+ end
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index a1c98f5e8ff..71a53e6f0d6 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -61,6 +61,7 @@ module API
# id (required) - The ID of a project
# noteable_id (required) - The ID of an issue or snippet
# body (required) - The content of a note
+ # created_at (optional) - The date
# Example Request:
# POST /projects/:id/issues/:noteable_id/notes
# POST /projects/:id/snippets/:noteable_id/notes
@@ -73,6 +74,10 @@ module API
noteable_id: params[noteable_id_str]
}
+ if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
+ opts[:created_at] = params[:created_at]
+ end
+
@note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if @note.valid?
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
index 4fc3443ac68..5f8ff01b0a9 100644
--- a/lib/award_emoji.rb
+++ b/lib/award_emoji.rb
@@ -14,19 +14,29 @@ class AwardEmoji
food_drink: "Food"
}.with_indifferent_access
+ CATEGORY_ALIASES = {
+ symbols: "objects_symbols",
+ foods: "food_drink",
+ travel: "travel_places"
+ }.with_indifferent_access
+
def self.normilize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
- @emoji_by_category = {}
+ @emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
- @emoji_by_category[data["category"]] ||= []
- @emoji_by_category[data["category"]] << data
+ # Skip Fitzpatrick(tone) modifiers
+ next if data["category"] == "modifier"
+
+ category = CATEGORY_ALIASES[data["category"]] || data["category"]
+
+ @emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index db44a9a44d4..5e2fb863a8f 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -79,33 +79,6 @@ module Gitlab
'rm-project', "#{name}.git"])
end
- # Add repository branch from passed ref
- #
- # path - project path with namespace
- # branch_name - new branch name
- # ref - HEAD for new branch
- #
- # Ex.
- # add_branch("gitlab/gitlab-ci", "4-0-stable", "master")
- #
- def add_branch(path, branch_name, ref)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'create-branch',
- "#{path}.git", branch_name, ref])
- end
-
- # Remove repository branch
- #
- # path - project path with namespace
- # branch_name - branch name to remove
- #
- # Ex.
- # rm_branch("gitlab/gitlab-ci", "4-0-stable")
- #
- def rm_branch(path, branch_name)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-branch',
- "#{path}.git", branch_name])
- end
-
# Add repository tag from passed ref
#
# path - project path with namespace
@@ -124,19 +97,6 @@ module Gitlab
Gitlab::Utils.system_silent(cmd)
end
- # Remove repository tag
- #
- # path - project path with namespace
- # tag_name - tag name to remove
- #
- # Ex.
- # rm_tag("gitlab/gitlab-ci", "v4.0")
- #
- def rm_tag(path, tag_name)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-tag',
- "#{path}.git", tag_name])
- end
-
# Gc repository
#
# path - project path with namespace
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 1acc22fe5bf..f44d1b3a44e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -34,7 +34,8 @@ module Gitlab
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
- akismet_enabled: false
+ akismet_enabled: false,
+ repository_checks_enabled: true,
)
end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index c2260a5f7ac..ffe49364379 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -52,11 +52,6 @@ module Gitlab
private
- def redis
- # Maybe someday we want to use a connection pool...
- @redis ||= Redis.new(url: Gitlab::RedisConfig.url)
- end
-
def redis_key
"gitlab:exclusive_lease:#{@key}"
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
new file mode 100644
index 00000000000..5ebaad6ca6e
--- /dev/null
+++ b/lib/gitlab/gon_helper.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module GonHelper
+ 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
+ end
+end
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb
index 18523e0aefe..8bdc89a7751 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/note_data_builder.rb
@@ -59,8 +59,7 @@ module Gitlab
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
- base_data[:object_attributes][:url] =
- Gitlab::UrlBuilder.new(:note).build(note.id)
+ base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note)
base_data
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 319447669dc..5c352c96de5 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,6 +1,8 @@
module Gitlab
class Redis
CACHE_NAMESPACE = 'cache:gitlab'
+ SESSION_NAMESPACE = 'session:gitlab'
+ SIDEKIQ_NAMESPACE = 'resque:gitlab'
attr_reader :url
diff --git a/lib/gitlab/repository_check_logger.rb b/lib/gitlab/repository_check_logger.rb
new file mode 100644
index 00000000000..485b596ca57
--- /dev/null
+++ b/lib/gitlab/repository_check_logger.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class RepositoryCheckLogger < Gitlab::Logger
+ def self.file_name_noext
+ 'repocheck'
+ end
+ end
+end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index f301d42939d..f1943222edf 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -4,50 +4,58 @@ module Gitlab
include GitlabRoutingHelper
include ActionView::RecordIdentifier
- def initialize(type)
- @type = type
- end
+ attr_reader :object
- def build(id)
- case @type
- when :issue
- build_issue_url(id)
- when :merge_request
- build_merge_request_url(id)
- when :note
- build_note_url(id)
+ def self.build(object)
+ new(object).url
+ end
+ def url
+ case object
+ when Commit
+ commit_url
+ when Issue
+ issue_url(object)
+ when MergeRequest
+ merge_request_url(object)
+ when Note
+ note_url
+ else
+ raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
end
private
- def build_issue_url(id)
- issue = Issue.find(id)
- issue_url(issue)
+ def initialize(object)
+ @object = object
end
- def build_merge_request_url(id)
- merge_request = MergeRequest.find(id)
- merge_request_url(merge_request)
+ def commit_url(opts = {})
+ return '' if object.project.nil?
+
+ namespace_project_commit_url({
+ namespace_id: object.project.namespace,
+ project_id: object.project,
+ id: object.id
+ }.merge!(opts))
end
- def build_note_url(id)
- note = Note.find(id)
- if note.for_commit?
- namespace_project_commit_url(namespace_id: note.project.namespace,
- id: note.commit_id,
- project_id: note.project,
- anchor: dom_id(note))
- elsif note.for_issue?
- issue = Issue.find(note.noteable_id)
- issue_url(issue, anchor: dom_id(note))
- elsif note.for_merge_request?
- merge_request = MergeRequest.find(note.noteable_id)
- merge_request_url(merge_request, anchor: dom_id(note))
- elsif note.for_snippet?
- snippet = Snippet.find(note.noteable_id)
- project_snippet_url(snippet, anchor: dom_id(note))
+ def note_url
+ if object.for_commit?
+ commit_url(id: object.commit_id, anchor: dom_id(object))
+
+ elsif object.for_issue?
+ issue = Issue.find(object.noteable_id)
+ issue_url(issue, anchor: dom_id(object))
+
+ elsif object.for_merge_request?
+ merge_request = MergeRequest.find(object.noteable_id)
+ merge_request_url(merge_request, anchor: dom_id(object))
+
+ elsif object.for_snippet?
+ snippet = Snippet.find(object.noteable_id)
+ project_snippet_url(snippet, anchor: dom_id(object))
end
end
end
diff --git a/lib/support/nginx/gitlab_ci b/lib/support/nginx/gitlab_ci
deleted file mode 100644
index bf05edfd780..00000000000
--- a/lib/support/nginx/gitlab_ci
+++ /dev/null
@@ -1,29 +0,0 @@
-# GITLAB CI
-server {
- listen 80 default_server; # e.g., listen 192.168.1.1:80;
- server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com;
-
- access_log /var/log/nginx/gitlab_ci_access.log;
- error_log /var/log/nginx/gitlab_ci_error.log;
-
- # expose API to fix runners
- location /api {
- proxy_read_timeout 300;
- proxy_connect_timeout 300;
- proxy_redirect off;
- proxy_set_header X-Real-IP $remote_addr;
-
- # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
- resolver 8.8.8.8 8.8.4.4;
- proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
- }
-
- # redirect all other CI requests
- location / {
- return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
- }
-
- # adjust this to match the largest build log your runners might submit,
- # set to 0 to disable limit
- client_max_body_size 10m;
-} \ No newline at end of file
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 4cbccf2ca89..48baecfd2a2 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -14,7 +14,7 @@ namespace :gitlab do
puts ""
end
- Rake::Task["db:setup"].invoke
+ Rake::Task["db:reset"].invoke
Rake::Task["add_limits_mysql"].invoke
Rake::Task["setup_postgresql"].invoke
Rake::Task["db:seed_fu"].invoke
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 410b993fdfb..28cf804c1b2 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -12,13 +12,13 @@ describe AutocompleteController do
project.team << [user, :master]
end
- let(:body) { JSON.parse(response.body) }
-
describe 'GET #users with project ID' do
before do
get(:users, project_id: project.id)
end
+ let(:body) { JSON.parse(response.body) }
+
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq 1 }
it { expect(body.map { |u| u["username"] }).to include(user.username) }
@@ -143,4 +143,24 @@ describe AutocompleteController do
it { expect(body.size).to eq 0 }
end
end
+
+ context 'author of issuable included' do
+ before do
+ sign_in(user)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it 'includes the author' do
+ get(:users, author_id: non_member.id)
+
+ expect(body.first["username"]).to eq non_member.username
+ end
+
+ it 'rejects non existent user ids' do
+ get(:users, author_id: 99999)
+
+ expect(body.collect { |u| u['id'] }).not_to include(99999)
+ end
+ end
end
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index b6573f105dc..3a82083717f 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -1,7 +1,17 @@
require 'spec_helper'
describe Profiles::KeysController do
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+
+ describe '#new' do
+ before { sign_in(user) }
+
+ it 'redirect to #index' do
+ get :new
+
+ expect(response).to redirect_to(profile_keys_path)
+ end
+ end
describe "#get_keys" do
describe "non existant user" do
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
new file mode 100644
index 00000000000..ac6eb0a7897
--- /dev/null
+++ b/spec/factories/commits.rb
@@ -0,0 +1,12 @@
+require_relative '../support/repo_helpers'
+
+FactoryGirl.define do
+ factory :commit do
+ git_commit RepoHelpers.sample_commit
+ project factory: :empty_project
+
+ initialize_with do
+ new(git_commit, project)
+ end
+ end
+end
diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb
new file mode 100644
index 00000000000..7700b15d538
--- /dev/null
+++ b/spec/factories/oauth_access_tokens.rb
@@ -0,0 +1,22 @@
+# == 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
+#
+
+FactoryGirl.define do
+ factory :oauth_access_token do
+ resource_owner
+ application
+ token '123456'
+ end
+end
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
new file mode 100644
index 00000000000..d116a573830
--- /dev/null
+++ b/spec/factories/oauth_applications.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
+ name { FFaker::Name.name }
+ uid { FFaker::Name.name }
+ redirect_uri { FFaker::Internet.uri('http') }
+ owner
+ owner_type 'User'
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index a5c60c51c5b..a9b2148bd2a 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -1,7 +1,7 @@
FactoryGirl.define do
sequence(:name) { FFaker::Name.name }
- factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do
+ factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do
email { FFaker::Internet.email }
name
sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" }
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
new file mode 100644
index 00000000000..661fb761809
--- /dev/null
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -0,0 +1,43 @@
+require 'rails_helper'
+
+feature 'Admin uses repository checks', feature: true do
+ before { login_as :admin }
+
+ scenario 'to trigger a single check' do
+ project = create(:empty_project)
+ visit_admin_project_page(project)
+
+ page.within('.repository-check') do
+ click_button 'Trigger repository check'
+ end
+
+ expect(page).to have_content('Repository check was triggered')
+ end
+
+ scenario 'to see a single failed repository check' do
+ project = create(:empty_project)
+ project.update_columns(
+ last_repository_check_failed: true,
+ last_repository_check_at: Time.now,
+ )
+ visit_admin_project_page(project)
+
+ page.within('.alert') do
+ expect(page.text).to match(/Last repository check \(.* ago\) failed/)
+ end
+ end
+
+ scenario 'to clear all repository checks', js: true do
+ visit admin_application_settings_path
+
+ expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
+
+ click_link 'Clear all repository checks'
+
+ expect(page).to have_content('Started asynchronous removal of all repository check states.')
+ end
+
+ def visit_admin_project_page(project)
+ visit admin_namespace_project_path(project.namespace, project)
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 79000666ccc..1ce0024e93c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -45,7 +45,7 @@ describe 'Issues', feature: true do
project: project)
end
- it 'allows user to select unasigned', js: true do
+ it 'allows user to select unassigned', js: true do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
expect(page).to have_content "Assignee #{@user.name}"
@@ -64,6 +64,18 @@ describe 'Issues', feature: true do
end
end
+ describe 'Issue info' do
+ it 'excludes award_emoji from comment count' do
+ issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
+ create(:upvote_note, noteable: issue)
+
+ visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
+
+ expect(page).to have_content 'foobar'
+ expect(page.all('.issue-no-comments').first.text).to eq "0"
+ end
+ end
+
describe 'Filter issue' do
before do
['foobar', 'barbaz', 'gitlab'].each do |title|
@@ -187,7 +199,7 @@ describe 'Issues', feature: true do
describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
- context 'by autorized user' do
+ context 'by authorized user' do
it 'allows user to select unassigned', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 5f855ccc701..389812ff7e1 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -4,6 +4,20 @@ describe 'Comments', feature: true do
include RepoHelpers
include WaitForAjax
+ describe 'On merge requests page', feature: true do
+ it 'excludes award_emoji from comment count' do
+ merge_request = create(:merge_request)
+ project = merge_request.source_project
+ create(:upvote_note, noteable: merge_request, project: project)
+
+ login_as :admin
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ expect(merge_request.mr_and_commit_notes.count).to eq 1
+ expect(page.all('.merge-request-no-comments').first.text).to eq "0"
+ end
+ end
+
describe 'On a merge request', js: true, feature: true do
let!(:merge_request) { create(:merge_request) }
let!(:project) { merge_request.source_project }
@@ -129,6 +143,17 @@ describe 'Comments', feature: true do
end
end
end
+
+ describe 'comment info' do
+ it 'excludes award_emoji from comment count' do
+ create(:upvote_note, noteable: merge_request, project: project)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(merge_request.mr_and_commit_notes.count).to eq 2
+ expect(find('.notes-tab span.badge').text).to eq "1"
+ end
+ end
end
describe 'On a merge request diff', js: true, feature: true do
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
new file mode 100644
index 00000000000..1a5a9059dbd
--- /dev/null
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe 'Profile > Applications', feature: true do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'User manages applications', js: true do
+ it 'deletes an application' do
+ create(:oauth_application, owner: user)
+ visit oauth_applications_path
+
+ page.within('.oauth-applications') do
+ expect(page).to have_content('Your applications (1)')
+ click_button 'Destroy'
+ end
+
+ expect(page).to have_content('The application was deleted successfully')
+ expect(page).to have_content('Your applications (0)')
+ expect(page).to have_content('Authorized applications (0)')
+ end
+
+ it 'deletes an authorized application' do
+ create(:oauth_access_token, resource_owner: user)
+ visit oauth_applications_path
+
+ page.within('.oauth-authorized-applications') do
+ expect(page).to have_content('Authorized applications (1)')
+ click_button 'Revoke'
+ end
+
+ expect(page).to have_content('The application was revoked access.')
+ expect(page).to have_content('Your applications (0)')
+ expect(page).to have_content('Authorized applications (0)')
+ end
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 3e6289a46b1..029a11ea43c 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -10,6 +10,10 @@ describe "Search", feature: true do
visit search_path
end
+ it 'top right search form is not present' do
+ expect(page).not_to have_selector('.search')
+ end
+
describe 'searching for Projects' do
it 'finds a project' do
page.within '.search-holder' do
diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee
index 050b6e362c6..dd160e821b3 100644
--- a/spec/javascripts/notes_spec.js.coffee
+++ b/spec/javascripts/notes_spec.js.coffee
@@ -1,4 +1,5 @@
#= require notes
+#= require gl_form
window.gon = {}
window.disableButtonIfEmptyField = -> null
diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb
index 330678f7f16..88c22912950 100644
--- a/spec/lib/award_emoji_spec.rb
+++ b/spec/lib/award_emoji_spec.rb
@@ -16,4 +16,11 @@ describe AwardEmoji do
end
end
end
+
+ describe '.emoji_by_category' do
+ it "only contains known categories" do
+ undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys
+ expect(undefined_categories).to be_empty
+ end
+ end
end
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb
index da652677443..f093d0a0d8b 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/note_data_builder_spec.rb
@@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
- let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
before(:each) do
expect(data).to have_key(:object_attributes)
expect(data[:object_attributes]).to have_key(:url)
- expect(data[:object_attributes][:url]).to eq(note_url)
+ expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note))
expect(data[:object_kind]).to eq('note')
expect(data[:user]).to eq(user.hook_attrs)
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index f023be6ae45..6ffc0d6e658 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -1,77 +1,110 @@
require 'spec_helper'
describe Gitlab::UrlBuilder, lib: true do
- describe 'When asking for an issue' do
- it 'returns the issue url' do
- issue = create(:issue)
- url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
- expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
- end
- end
+ describe '.build' do
+ context 'when passing a Commit' do
+ it 'returns a proper URL' do
+ commit = build_stubbed(:commit)
- describe 'When asking for an merge request' do
- it 'returns the merge request url' do
- merge_request = create(:merge_request)
- url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
+ url = described_class.build(commit)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
+ end
end
- end
- describe 'When asking for a note on commit' do
- let(:note) { create(:note_on_commit) }
- let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
+ context 'when passing an Issue' do
+ it 'returns a proper URL' do
+ issue = build_stubbed(:issue, iid: 42)
- it 'returns the note url' do
- expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ url = described_class.build(issue)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
+ end
end
- end
- describe 'When asking for a note on commit diff' do
- let(:note) { create(:note_on_commit_diff) }
- let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
+ context 'when passing a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = build_stubbed(:merge_request, iid: 42)
+
+ url = described_class.build(merge_request)
- it 'returns the note url' do
- expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
+ end
end
- end
- describe 'When asking for a note on issue' do
- let(:issue) { create(:issue) }
- let(:note) { create(:note_on_issue, noteable_id: issue.id) }
- let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
+ context 'when passing a Note' do
+ context 'on a Commit' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:note_on_commit)
- it 'returns the note url' do
- expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
- end
- end
+ url = described_class.build(note)
- describe 'When asking for a note on merge request' do
- let(:merge_request) { create(:merge_request) }
- let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) }
- let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
- it 'returns the note url' do
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
- end
- end
+ context 'on a CommitDiff' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:note_on_commit_diff)
- describe 'When asking for a note on merge request diff' do
- let(:merge_request) { create(:merge_request) }
- let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) }
- let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
+ url = described_class.build(note)
- it 'returns the note url' do
- expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
- end
- end
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on an Issue' do
+ it 'returns a proper URL' do
+ issue = create(:issue, iid: 42)
+ note = build_stubbed(:note_on_issue, noteable: issue)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequestDiff' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a ProjectSnippet' do
+ it 'returns a proper URL' do
+ project_snippet = create(:project_snippet)
+ note = build_stubbed(:note_on_project_snippet, noteable: project_snippet)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
+ end
+ end
- describe 'When asking for a note on project snippet' do
- let(:snippet) { create(:project_snippet) }
- let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) }
- let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
+ context 'on another object' do
+ it 'returns a proper URL' do
+ project = build_stubbed(:project)
- it 'returns the note url' do
- expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
+ expect { described_class.build(project) }.
+ to raise_error(NotImplementedError, 'No URL builder defined for Project')
+ end
+ end
end
end
end
diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb
new file mode 100644
index 00000000000..583bf15176f
--- /dev/null
+++ b/spec/mailers/repository_check_mailer_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe RepositoryCheckMailer do
+ include EmailSpec::Matchers
+
+ describe '.notify' do
+ it 'emails all admins' do
+ admins = create_list(:admin, 3)
+
+ mail = described_class.notify(1)
+
+ expect(mail).to deliver_to admins.map(&:email)
+ end
+
+ it 'mentions the number of failed checks' do
+ mail = described_class.notify(3)
+
+ expect(mail).to have_subject '3 projects failed their last repository check'
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 15052aaca28..fac516f9568 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -191,12 +191,19 @@ describe Issue, models: true do
end
describe '#related_branches' do
- it "selects the right branches" do
+ it 'selects the right branches' do
allow(subject.project.repository).to receive(:branch_names).
- and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name])
+ and_return(['mpempe', "#{subject.iid}mepmep", subject.to_branch_name])
expect(subject.related_branches).to eq([subject.to_branch_name])
end
+
+ it 'excludes stable branches from the related branches' do
+ allow(subject.project.repository).to receive(:branch_names).
+ and_return(["#{subject.iid}-0-stable"])
+
+ expect(subject.related_branches).to eq []
+ end
end
it_behaves_like 'an editable mentionable' do
@@ -210,11 +217,11 @@ describe Issue, models: true do
let(:subject) { create :issue }
end
- describe "#to_branch_name" do
+ describe '#to_branch_name' do
let(:issue) { create(:issue, title: 'a' * 30) }
- it "starts with the issue iid" do
- expect(issue.to_branch_name).to match /-#{issue.iid}\z/
+ it 'starts with the issue iid' do
+ expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
end
end
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index c34b2487ecf..31b2c90122d 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -21,74 +21,232 @@
require 'spec_helper'
describe BambooService, models: true do
- describe "Associations" do
+ describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
- describe "Execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- context "when a password was previously set" do
- before do
- @bamboo_service = BambooService.create(
- project: create(:project),
- properties: {
- bamboo_url: 'http://gitlab.com',
- username: 'mic',
- password: "password"
- }
- )
+ describe 'Validations' do
+ describe '#bamboo_url' do
+ it 'does not validate the presence of bamboo_url if service is not active' do
+ bamboo_service = service
+ bamboo_service.active = false
+
+ expect(bamboo_service).not_to validate_presence_of(:bamboo_url)
+ end
+
+ it 'validates the presence of bamboo_url if service is active' do
+ bamboo_service = service
+ bamboo_service.active = true
+
+ expect(bamboo_service).to validate_presence_of(:bamboo_url)
+ end
+ end
+
+ describe '#build_key' do
+ it 'does not validate the presence of build_key if service is not active' do
+ bamboo_service = service
+ bamboo_service.active = false
+
+ expect(bamboo_service).not_to validate_presence_of(:build_key)
end
-
- it "reset password if url changed" do
- @bamboo_service.bamboo_url = 'http://gitlab1.com'
- @bamboo_service.save
- expect(@bamboo_service.password).to be_nil
+
+ it 'validates the presence of build_key if service is active' do
+ bamboo_service = service
+ bamboo_service.active = true
+
+ expect(bamboo_service).to validate_presence_of(:build_key)
+ end
+ end
+
+ describe '#username' do
+ it 'does not validate the presence of username if service is not active' do
+ bamboo_service = service
+ bamboo_service.active = false
+
+ expect(bamboo_service).not_to validate_presence_of(:username)
+ end
+
+ it 'does not validate the presence of username if username is nil' do
+ bamboo_service = service
+ bamboo_service.active = true
+ bamboo_service.password = nil
+
+ expect(bamboo_service).not_to validate_presence_of(:username)
+ end
+
+ it 'validates the presence of username if service is active and username is present' do
+ bamboo_service = service
+ bamboo_service.active = true
+ bamboo_service.password = 'secret'
+
+ expect(bamboo_service).to validate_presence_of(:username)
end
-
- it "does not reset password if username changed" do
- @bamboo_service.username = "some_name"
- @bamboo_service.save
- expect(@bamboo_service.password).to eq("password")
+ end
+
+ describe '#password' do
+ it 'does not validate the presence of password if service is not active' do
+ bamboo_service = service
+ bamboo_service.active = false
+
+ expect(bamboo_service).not_to validate_presence_of(:password)
end
- it "does not reset password if new url is set together with password, even if it's the same password" do
- @bamboo_service.bamboo_url = 'http://gitlab_edited.com'
- @bamboo_service.password = 'password'
- @bamboo_service.save
- expect(@bamboo_service.password).to eq("password")
- expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com")
+ it 'does not validate the presence of password if username is nil' do
+ bamboo_service = service
+ bamboo_service.active = true
+ bamboo_service.username = nil
+
+ expect(bamboo_service).not_to validate_presence_of(:password)
end
- it "should reset password if url changed, even if setter called multiple times" do
- @bamboo_service.bamboo_url = 'http://gitlab1.com'
- @bamboo_service.bamboo_url = 'http://gitlab1.com'
- @bamboo_service.save
- expect(@bamboo_service.password).to be_nil
+ it 'validates the presence of password if service is active and username is present' do
+ bamboo_service = service
+ bamboo_service.active = true
+ bamboo_service.username = 'john'
+
+ expect(bamboo_service).to validate_presence_of(:password)
end
end
-
- context "when no password was previously set" do
- before do
- @bamboo_service = BambooService.create(
- project: create(:project),
- properties: {
- bamboo_url: 'http://gitlab.com',
- username: 'mic'
- }
- )
+ end
+
+ describe 'Callbacks' do
+ describe 'before_update :reset_password' do
+ context 'when a password was previously set' do
+ it 'resets password if url changed' do
+ bamboo_service = service
+
+ bamboo_service.bamboo_url = 'http://gitlab1.com'
+ bamboo_service.save
+
+ expect(bamboo_service.password).to be_nil
+ end
+
+ it 'does not reset password if username changed' do
+ bamboo_service = service
+
+ bamboo_service.username = 'some_name'
+ bamboo_service.save
+
+ expect(bamboo_service.password).to eq('password')
+ end
+
+ it "does not reset password if new url is set together with password, even if it's the same password" do
+ bamboo_service = service
+
+ bamboo_service.bamboo_url = 'http://gitlab_edited.com'
+ bamboo_service.password = 'password'
+ bamboo_service.save
+
+ expect(bamboo_service.password).to eq('password')
+ expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
+ end
end
- it "saves password if new url is set together with password" do
- @bamboo_service.bamboo_url = 'http://gitlab_edited.com'
- @bamboo_service.password = 'password'
- @bamboo_service.save
- expect(@bamboo_service.password).to eq("password")
- expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com")
+ it 'saves password if new url is set together with password when no password was previously set' do
+ bamboo_service = service
+ bamboo_service.password = nil
+
+ bamboo_service.bamboo_url = 'http://gitlab_edited.com'
+ bamboo_service.password = 'password'
+ bamboo_service.save
+
+ expect(bamboo_service.password).to eq('password')
+ expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
end
+ end
+ end
+
+ describe '#build_page' do
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
+
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ end
+
+ it 'returns a specific URL when response has no results' do
+ stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ end
+
+ it 'returns a build URL when bamboo_url has no trailing slash' do
+ stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
+
+ expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ end
+
+ it 'returns a build URL when bamboo_url has a trailing slash' do
+ stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
+
+ expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ end
+ end
+
+ describe '#commit_status' do
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ expect(service.commit_status('123', 'unused')).to eq(:error)
+ end
+
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
+
+ expect(service.commit_status('123', 'unused')).to eq('pending')
+ end
+
+ it 'sets commit status to "pending" when response has no results' do
+ stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+
+ expect(service.commit_status('123', 'unused')).to eq('pending')
+ end
+
+ it 'sets commit status to "success" when build state contains Success' do
+ stub_request(build_state: 'YAY Success!')
+ expect(service.commit_status('123', 'unused')).to eq('success')
end
+
+ it 'sets commit status to "failed" when build state contains Failed' do
+ stub_request(build_state: 'NO Failed!')
+
+ expect(service.commit_status('123', 'unused')).to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build state contains Pending' do
+ stub_request(build_state: 'NO Pending!')
+
+ expect(service.commit_status('123', 'unused')).to eq('pending')
+ end
+
+ it 'sets commit status to :error when build state is unknown' do
+ stub_request(build_state: 'FOO BAR!')
+
+ expect(service.commit_status('123', 'unused')).to eq(:error)
+ end
+ end
+
+ def service(bamboo_url: 'http://gitlab.com')
+ described_class.create(
+ project: build_stubbed(:empty_project),
+ properties: {
+ bamboo_url: bamboo_url,
+ username: 'mic',
+ password: 'password',
+ build_key: 'foo'
+ }
+ )
+ end
+
+ def stub_request(status: 200, body: nil, build_state: 'success')
+ bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic'
+ body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
+
+ WebMock.stub_request(:get, bamboo_full_url).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
end
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index 2ccbff553f0..7c23c2efccd 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -3,9 +3,10 @@ require 'spec_helper'
describe BuildsEmailService do
let(:build) { create(:ci_build) }
let(:data) { Gitlab::BuildDataBuilder.build(build) }
- let(:service) { BuildsEmailService.new }
+ let!(:project) { create(:project, :public, ci_id: 1) }
+ let(:service) { described_class.new(project: project, active: true) }
- describe :execute do
+ describe '#execute' do
it 'sends email' do
service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed'
@@ -40,4 +41,36 @@ describe BuildsEmailService do
service.execute(data)
end
end
+
+ describe 'validations' do
+
+ context 'when pusher is not added' do
+ before { service.add_pusher = false }
+
+ it 'does not allow empty recipient input' do
+ service.recipients = ''
+ expect(service.valid?).to be false
+ end
+
+ it 'does allow non-empty recipient input' do
+ service.recipients = 'test@example.com'
+ expect(service.valid?).to be true
+ end
+
+ end
+
+ context 'when pusher is added' do
+ before { service.add_pusher = true }
+
+ it 'does allow empty recipient input' do
+ service.recipients = ''
+ expect(service.valid?).to be true
+ end
+
+ it 'does allow non-empty recipient input' do
+ service.recipients = 'test@example.com'
+ expect(service.valid?).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index f26b47a856c..bc7423cee69 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -21,73 +21,220 @@
require 'spec_helper'
describe TeamcityService, models: true do
- describe "Associations" do
+ describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
- describe "Execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- context "when a password was previously set" do
- before do
- @teamcity_service = TeamcityService.create(
- project: create(:project),
- properties: {
- teamcity_url: 'http://gitlab.com',
- username: 'mic',
- password: "password"
- }
- )
+ describe 'Validations' do
+ describe '#teamcity_url' do
+ it 'does not validate the presence of teamcity_url if service is not active' do
+ teamcity_service = service
+ teamcity_service.active = false
+
+ expect(teamcity_service).not_to validate_presence_of(:teamcity_url)
end
-
- it "reset password if url changed" do
- @teamcity_service.teamcity_url = 'http://gitlab1.com'
- @teamcity_service.save
- expect(@teamcity_service.password).to be_nil
+
+ it 'validates the presence of teamcity_url if service is active' do
+ teamcity_service = service
+ teamcity_service.active = true
+
+ expect(teamcity_service).to validate_presence_of(:teamcity_url)
+ end
+ end
+
+ describe '#build_type' do
+ it 'does not validate the presence of build_type if service is not active' do
+ teamcity_service = service
+ teamcity_service.active = false
+
+ expect(teamcity_service).not_to validate_presence_of(:build_type)
+ end
+
+ it 'validates the presence of build_type if service is active' do
+ teamcity_service = service
+ teamcity_service.active = true
+
+ expect(teamcity_service).to validate_presence_of(:build_type)
end
-
- it "does not reset password if username changed" do
- @teamcity_service.username = "some_name"
- @teamcity_service.save
- expect(@teamcity_service.password).to eq("password")
+ end
+
+ describe '#username' do
+ it 'does not validate the presence of username if service is not active' do
+ teamcity_service = service
+ teamcity_service.active = false
+
+ expect(teamcity_service).not_to validate_presence_of(:username)
end
- it "does not reset password if new url is set together with password, even if it's the same password" do
- @teamcity_service.teamcity_url = 'http://gitlab_edited.com'
- @teamcity_service.password = 'password'
- @teamcity_service.save
- expect(@teamcity_service.password).to eq("password")
- expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com")
+ it 'does not validate the presence of username if username is nil' do
+ teamcity_service = service
+ teamcity_service.active = true
+ teamcity_service.password = nil
+
+ expect(teamcity_service).not_to validate_presence_of(:username)
end
- it "should reset password if url changed, even if setter called multiple times" do
- @teamcity_service.teamcity_url = 'http://gitlab1.com'
- @teamcity_service.teamcity_url = 'http://gitlab1.com'
- @teamcity_service.save
- expect(@teamcity_service.password).to be_nil
+ it 'validates the presence of username if service is active and username is present' do
+ teamcity_service = service
+ teamcity_service.active = true
+ teamcity_service.password = 'secret'
+
+ expect(teamcity_service).to validate_presence_of(:username)
end
end
-
- context "when no password was previously set" do
- before do
- @teamcity_service = TeamcityService.create(
- project: create(:project),
- properties: {
- teamcity_url: 'http://gitlab.com',
- username: 'mic'
- }
- )
+
+ describe '#password' do
+ it 'does not validate the presence of password if service is not active' do
+ teamcity_service = service
+ teamcity_service.active = false
+
+ expect(teamcity_service).not_to validate_presence_of(:password)
+ end
+
+ it 'does not validate the presence of password if username is nil' do
+ teamcity_service = service
+ teamcity_service.active = true
+ teamcity_service.username = nil
+
+ expect(teamcity_service).not_to validate_presence_of(:password)
end
- it "saves password if new url is set together with password" do
- @teamcity_service.teamcity_url = 'http://gitlab_edited.com'
- @teamcity_service.password = 'password'
- @teamcity_service.save
- expect(@teamcity_service.password).to eq("password")
- expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com")
+ it 'validates the presence of password if service is active and username is present' do
+ teamcity_service = service
+ teamcity_service.active = true
+ teamcity_service.username = 'john'
+
+ expect(teamcity_service).to validate_presence_of(:password)
end
end
end
+
+ describe 'Callbacks' do
+ describe 'before_update :reset_password' do
+ context 'when a password was previously set' do
+ it 'resets password if url changed' do
+ teamcity_service = service
+
+ teamcity_service.teamcity_url = 'http://gitlab1.com'
+ teamcity_service.save
+
+ expect(teamcity_service.password).to be_nil
+ end
+
+ it 'does not reset password if username changed' do
+ teamcity_service = service
+
+ teamcity_service.username = 'some_name'
+ teamcity_service.save
+
+ expect(teamcity_service.password).to eq('password')
+ end
+
+ it "does not reset password if new url is set together with password, even if it's the same password" do
+ teamcity_service = service
+
+ teamcity_service.teamcity_url = 'http://gitlab_edited.com'
+ teamcity_service.password = 'password'
+ teamcity_service.save
+
+ expect(teamcity_service.password).to eq('password')
+ expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com')
+ end
+ end
+
+ it 'saves password if new url is set together with password when no password was previously set' do
+ teamcity_service = service
+ teamcity_service.password = nil
+
+ teamcity_service.teamcity_url = 'http://gitlab_edited.com'
+ teamcity_service.password = 'password'
+ teamcity_service.save
+
+ expect(teamcity_service.password).to eq('password')
+ expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com')
+ end
+ end
+ end
+
+ describe '#build_page' do
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
+
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo')
+ end
+
+ it 'returns a build URL when teamcity_url has no trailing slash' do
+ stub_request(body: %Q({"build":{"id":"666"}}))
+
+ expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ end
+
+ it 'returns a build URL when teamcity_url has a trailing slash' do
+ stub_request(body: %Q({"build":{"id":"666"}}))
+
+ expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ end
+ end
+
+ describe '#commit_status' do
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ expect(service.commit_status('123', 'unused')).to eq(:error)
+ end
+
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
+
+ expect(service.commit_status('123', 'unused')).to eq('pending')
+ end
+
+ it 'sets commit status to "success" when build status contains SUCCESS' do
+ stub_request(build_status: 'YAY SUCCESS!')
+
+ expect(service.commit_status('123', 'unused')).to eq('success')
+ end
+
+ it 'sets commit status to "failed" when build status contains FAILURE' do
+ stub_request(build_status: 'NO FAILURE!')
+
+ expect(service.commit_status('123', 'unused')).to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build status contains Pending' do
+ stub_request(build_status: 'NO Pending!')
+
+ expect(service.commit_status('123', 'unused')).to eq('pending')
+ end
+
+ it 'sets commit status to :error when build status is unknown' do
+ stub_request(build_status: 'FOO BAR!')
+
+ expect(service.commit_status('123', 'unused')).to eq(:error)
+ end
+ end
+
+ def service(teamcity_url: 'http://gitlab.com')
+ described_class.create(
+ project: build_stubbed(:empty_project),
+ properties: {
+ teamcity_url: teamcity_url,
+ username: 'mic',
+ password: 'password',
+ build_type: 'foo'
+ }
+ )
+ end
+
+ def stub_request(status: 200, body: nil, build_status: 'success')
+ teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
+
+ WebMock.stub_request(:get, teamcity_full_url).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index c3a4016fa49..c163001b7c1 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -94,6 +94,12 @@ describe Repository, models: true do
it { is_expected.to be_an Array }
+ it 'regex-escapes the query string' do
+ results = repository.search_files("test\\", 'master')
+
+ expect(results.first).not_to start_with('fatal:')
+ end
+
describe 'result' do
subject { results.first }
@@ -764,11 +770,9 @@ describe Repository, models: true do
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
+ expect(repository.rugged.tags).to receive(:delete).with('v1.1.0')
- expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
- with(repository.path_with_namespace, '8.5')
-
- repository.rm_tag('8.5')
+ repository.rm_tag('v1.1.0')
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 3d7a31cbb6a..f88e39cad9e 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
let(:non_member) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
@@ -320,13 +321,13 @@ describe API::API, api: true do
end
context 'when an admin or owner makes the request' do
- it "accepts the creation date to be set" do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
post api("/projects/#{project.id}/issues", user),
- title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago
+ title: 'new issue', labels: 'label, label2', created_at: creation_time
expect(response.status).to eq(201)
- # this take about a second, so probably not equal
- expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago
+ expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
end
@@ -477,6 +478,18 @@ describe API::API, api: true do
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the update date to be set' do
+ update_time = 2.weeks.ago
+ put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label3', state_event: 'close', updated_at: update_time
+ expect(response.status).to eq(200)
+
+ expect(json_response['labels']).to include 'label3'
+ expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time)
+ end
+ end
end
describe "DELETE /projects/:id/issues/:issue_id" do
@@ -569,4 +582,46 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST :id/issues/:issue_id/subscription' do
+ it 'subscribes to an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response.status).to eq(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response.status).to eq(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'DELETE :id/issues/:issue_id/subscription' do
+ it 'unsubscribes from an issue' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response.status).to eq(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ delete api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 25fa30b2f21..1fa7e76894f 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -516,6 +516,48 @@ describe API::API, api: true do
end
end
+ describe 'POST :id/merge_requests/:merge_request_id/subscription' do
+ it 'subscribes to a merge request' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response.status).to eq(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response.status).to eq(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ it 'unsubscribes from a merge request' do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response.status).to eq(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
def mr_with_later_created_and_updated_at_time
merge_request
merge_request.created_at += 1.hour
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index a467bc935af..ec9eda0a2ed 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -158,6 +158,19 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
expect(response.status).to eq(401)
end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ body: 'hi!', created_at: creation_time
+ expect(response.status).to eq(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
+ end
+ end
+
end
context "when noteable is a Snippet" do
diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb
index 5b7ba521812..477551f5036 100644
--- a/spec/services/delete_tag_service_spec.rb
+++ b/spec/services/delete_tag_service_spec.rb
@@ -6,21 +6,12 @@ describe DeleteTagService, services: true do
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
- let(:tag) { double(:tag, name: '8.5', target: 'abc123') }
-
describe '#execute' do
- before do
- allow(repository).to receive(:find_tag).and_return(tag)
- end
-
it 'removes the tag' do
- expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
- and_return(true)
-
expect(repository).to receive(:before_remove_tag)
expect(service).to receive(:success)
- service.execute('8.5')
+ service.execute('v1.1.0')
end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index c46259431aa..06017317339 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do
def transfer_project(project, user, new_namespace)
Projects::TransferService.new(project, user).execute(new_namespace)
end
+
+ context 'visibility level' do
+ let(:internal_group) { create(:group, :internal) }
+
+ before { internal_group.add_owner(user) }
+
+ context 'when namespace visibility level < project visibility level' do
+ let(:public_project) { create(:project, :public, namespace: user.namespace) }
+
+ before { transfer_project(public_project, user, internal_group) }
+
+ it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) }
+ end
+
+ context 'when namespace visibility level > project visibility level' do
+ let(:private_project) { create(:project, :private, namespace: user.namespace) }
+
+ before { transfer_project(private_project, user, internal_group) }
+
+ it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
+ end
+ end
+
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 0265dbe9c66..94ff3457902 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -4,6 +4,9 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" }
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
+ let(:project) { create(:project) }
+ let(:key) { create(:key, user: project.owner) }
+ let(:key_id) { key.shell_id }
context "as a resque worker" do
it "reponds to #perform" do
@@ -11,11 +14,43 @@ describe PostReceive do
end
end
- context "webhook" do
- let(:project) { create(:project) }
- let(:key) { create(:key, user: project.owner) }
- let(:key_id) { key.shell_id }
+ describe "#process_project_changes" do
+ before do
+ allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ end
+ context "branches" do
+ let(:changes) { "123456 789012 refs/heads/tést" }
+
+ it "should call GitTagPushService" do
+ expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ end
+ end
+
+ context "tags" do
+ let(:changes) { "123456 789012 refs/tags/tag" }
+
+ it "should call GitTagPushService" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
+ PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ end
+ end
+
+ context "merge-requests" do
+ let(:changes) { "123456 789012 refs/merge-requests/123" }
+
+ it "should not call any of the services" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ PostReceive.new.perform(pwd(project), key_id, base64_changes)
+ end
+ end
+ end
+
+ context "webhook" do
it "fetches the correct project" do
expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
new file mode 100644
index 00000000000..f486e45ddad
--- /dev/null
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe RepositoryCheck::BatchWorker do
+ subject { described_class.new }
+
+ it 'prefers projects that have never been checked' do
+ projects = create_list(:project, 3)
+ projects[0].update_column(:last_repository_check_at, 4.months.ago)
+ projects[2].update_column(:last_repository_check_at, 3.months.ago)
+
+ expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id))
+ end
+
+ it 'sorts projects by last_repository_check_at' do
+ projects = create_list(:project, 3)
+ projects[0].update_column(:last_repository_check_at, 2.months.ago)
+ projects[1].update_column(:last_repository_check_at, 4.months.ago)
+ projects[2].update_column(:last_repository_check_at, 3.months.ago)
+
+ expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id))
+ end
+
+ it 'excludes projects that were checked recently' do
+ projects = create_list(:project, 3)
+ projects[0].update_column(:last_repository_check_at, 2.days.ago)
+ projects[1].update_column(:last_repository_check_at, 2.months.ago)
+ projects[2].update_column(:last_repository_check_at, 3.days.ago)
+
+ expect(subject.perform).to eq([projects[1].id])
+ end
+
+ it 'does nothing when repository checks are disabled' do
+ create(:empty_project)
+ current_settings = double('settings', repository_checks_enabled: false)
+ expect(subject).to receive(:current_settings) { current_settings }
+
+ expect(subject.perform).to eq(nil)
+ end
+end
diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb
new file mode 100644
index 00000000000..a3b70c74787
--- /dev/null
+++ b/spec/workers/repository_check/clear_worker_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe RepositoryCheck::ClearWorker do
+ it 'clears repository check columns' do
+ project = create(:empty_project)
+ project.update_columns(
+ last_repository_check_failed: true,
+ last_repository_check_at: Time.now,
+ )
+
+ described_class.new.perform
+ project.reload
+
+ expect(project.last_repository_check_failed).to be_nil
+ expect(project.last_repository_check_at).to be_nil
+ end
+end