Commit 953bafff authored by Timothy Andrew's avatar Timothy Andrew

Merge remote-tracking branch 'origin/master' into 14566-confidential-issue-branches

parents f5ce601c a1497ba3
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- 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) - 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) - 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) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea). - 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) - 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) - 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) - 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 - Expose project badges in project settings
- Preserve time notes/comments have been updated at when moving issue - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
- Expose label description in API (Mariusz Jachimowicz) - 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 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 - Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling) - API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530 - 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 - 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 - Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu) - 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: 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 - Add default scope to projects to exclude projects pending deletion
- Allow to close merge requests which source projects(forks) are deleted.
- Ensure empty recipients are rejected in BuildsEmailService - Ensure empty recipients are rejected in BuildsEmailService
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling) - API: Fix milestone filtering by `iid` (Robert Schilling)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling) - API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups - 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 - 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.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
...@@ -39,11 +57,18 @@ v 8.7.0 (unreleased) ...@@ -39,11 +57,18 @@ v 8.7.0 (unreleased)
- Fix admin/projects when using visibility levels on search (PotHix) - Fix admin/projects when using visibility levels on search (PotHix)
- Build status notifications - Build status notifications
- API: Expose user location (Robert Schilling) - API: Expose user location (Robert Schilling)
- API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600 - Update number of Todos in the sidebar when it's marked as "Done". !3600
- Sanitize branch names created for confidential issues - Sanitize branch names created for confidential issues
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) - 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 - API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
- Diffs load at the correct point when linking from from number
- Selected diff rows highlight
- Fix emoji catgories in the emoji picker
v 8.6.6 v 8.6.6
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
......
...@@ -22,7 +22,17 @@ ...@@ -22,7 +22,17 @@
#= require cal-heatmap #= require cal-heatmap
#= require turbolinks #= require turbolinks
#= require autosave #= require autosave
#= require bootstrap #= require bootstrap/affix
#= require bootstrap/alert
#= require bootstrap/button
#= require bootstrap/collapse
#= require bootstrap/dropdown
#= require bootstrap/modal
#= require bootstrap/scrollspy
#= require bootstrap/tab
#= require bootstrap/transition
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2 #= require select2
#= require raphael #= require raphael
#= require g.raphael #= require g.raphael
......
...@@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> ...@@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
e.preventDefault() e.preventDefault()
$form = $(e.target).closest('form') $form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable() $submit_button = $form.find('input[type=submit], button[type=submit]')
return if $submit_button.attr('disabled')
$submit_button.disable()
$form.submit() $form.submit()
# If the user tabs to a submit button on a `js-quick-submit` form, display a # If the user tabs to a submit button on a `js-quick-submit` form, display a
......
...@@ -28,26 +28,26 @@ class Dispatcher ...@@ -28,26 +28,26 @@ class Dispatcher
new Todos() new Todos()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
new DropzoneInput($('.milestone-form')) new GLForm($('.milestone-form'))
when 'groups:milestones:new' when 'groups:milestones:new'
new ZenMode() new ZenMode()
when 'projects:compare:show' when 'projects:compare:show'
new Diff() new Diff()
when 'projects:issues:new','projects:issues:edit' when 'projects:issues:new','projects:issues:edit'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.issue-form')) new GLForm($('.issue-form'))
new IssuableForm($('.issue-form')) new IssuableForm($('.issue-form'))
when 'projects:merge_requests:new', 'projects:merge_requests:edit' when 'projects:merge_requests:new', 'projects:merge_requests:edit'
new Diff() new Diff()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.merge-request-form')) new GLForm($('.merge-request-form'))
new IssuableForm($('.merge-request-form')) new IssuableForm($('.merge-request-form'))
when 'projects:tags:new' when 'projects:tags:new'
new ZenMode() new ZenMode()
new DropzoneInput($('.tag-form')) new GLForm($('.tag-form'))
when 'projects:releases:edit' when 'projects:releases:edit'
new ZenMode() new ZenMode()
new DropzoneInput($('.release-form')) new GLForm($('.release-form'))
when 'projects:merge_requests:show' when 'projects:merge_requests:show'
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
...@@ -137,7 +137,7 @@ class Dispatcher ...@@ -137,7 +137,7 @@ class Dispatcher
new Wikis() new Wikis()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ZenMode() new ZenMode()
new DropzoneInput($('.wiki-form')) new GLForm($('.wiki-form'))
when 'snippets' when 'snippets'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show' new ZenMode() if path[2] == 'show'
......
...@@ -15,11 +15,13 @@ class @DropzoneInput ...@@ -15,11 +15,13 @@ class @DropzoneInput
project_uploads_path = window.project_uploads_path or null project_uploads_path = window.project_uploads_path or null
max_file_size = gon.max_file_size or 10 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.wrap "<div class=\"div-dropzone\"></div>"
form_textarea.on 'paste', (event) => form_textarea.on 'paste', (event) =>
handlePaste(event) handlePaste(event)
$mdArea = $(form_textarea).closest('.md-area')
$(form).setupMarkdownPreview() $(form).setupMarkdownPreview()
form_dropzone = $(form).find('.div-dropzone') form_dropzone = $(form).find('.div-dropzone')
...@@ -49,17 +51,16 @@ class @DropzoneInput ...@@ -49,17 +51,16 @@ class @DropzoneInput
$(".div-dropzone-alert").alert "close" $(".div-dropzone-alert").alert "close"
dragover: -> dragover: ->
form_textarea.addClass "div-dropzone-focus" $mdArea.addClass 'is-dropzone-hover'
form.find(".div-dropzone-hover").css "opacity", 0.7 form.find(".div-dropzone-hover").css "opacity", 0.7
return return
dragleave: -> dragleave: ->
form_textarea.removeClass "div-dropzone-focus" $mdArea.removeClass 'is-dropzone-hover'
form.find(".div-dropzone-hover").css "opacity", 0 form.find(".div-dropzone-hover").css "opacity", 0
return return
drop: -> drop: ->
form_textarea.removeClass "div-dropzone-focus"
form.find(".div-dropzone-hover").css "opacity", 0 form.find(".div-dropzone-hover").css "opacity", 0
form_textarea.focus() form_textarea.focus()
return return
......
class @GLForm
constructor: (@form) ->
@textarea = @form.find('textarea.js-gfm-input')
# Before we start, we should clean up any previous data for this form
@destroy()
# Setup the form
@setupForm()
@form.data 'gl-form', @
destroy: ->
# Clean form listeners
@clearEventListeners()
@form.data 'gl-form', null
setupForm: ->
isNewForm = @form.is(':not(.gfm-form)')
@form.removeClass 'js-new-note-form'
if isNewForm
@form.find('.div-dropzone').remove()
@form.addClass('gfm-form')
disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button')
# remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup()
new DropzoneInput(@form)
autosize(@textarea)
# form and textarea event listeners
@addEventListeners()
# hide discard button
@form.find('.js-note-discard').hide()
@form.show()
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
addEventListeners: ->
@textarea.on 'focus', ->
$(@).closest('.md-area').addClass 'is-focused'
@textarea.on 'blur', ->
$(@).closest('.md-area').removeClass 'is-focused'
...@@ -10,6 +10,9 @@ class @Issue ...@@ -10,6 +10,9 @@ class @Issue
@initTaskList() @initTaskList()
@initIssueBtnEventListeners() @initIssueBtnEventListeners()
@initMergeRequests()
@initRelatedBranches()
initTaskList: -> initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable') $('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
...@@ -69,3 +72,23 @@ class @Issue ...@@ -69,3 +72,23 @@ class @Issue
type: 'PATCH' type: 'PATCH'
url: $('form.js-issuable-update').attr('action') url: $('form.js-issuable-update').attr('action')
data: patchData data: patchData
initMergeRequests: ->
$container = $('#merge-requests')
$.getJSON($container.data('url'))
.error ->
new Flash('Failed to load referenced merge requests', 'alert')
.success (data) ->
if 'html' of data
$container.html(data.html)
initRelatedBranches: ->
$container = $('#related-branches')
$.getJSON($container.data('url'))
.error ->
new Flash('Failed to load related branches', 'alert')
.success (data) ->
if 'html' of data
$container.html(data.html)
...@@ -34,7 +34,7 @@ class @LabelsSelect ...@@ -34,7 +34,7 @@ class @LabelsSelect
labelHTMLTemplate = _.template( labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %> '<% _.each(labels, function(label){ %>
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> <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 %> <%= label.title %>
</span> </span>
</a> </a>
...@@ -165,6 +165,8 @@ class @LabelsSelect ...@@ -165,6 +165,8 @@ class @LabelsSelect
.html(template) .html(template)
$sidebarCollapsedValue.text(labelCount) $sidebarCollapsedValue.text(labelCount)
$('.has-tooltip', $value).tooltip(container: 'body')
$value $value
.find('a') .find('a')
.each((i) -> .each((i) ->
...@@ -218,7 +220,7 @@ class @LabelsSelect ...@@ -218,7 +220,7 @@ class @LabelsSelect
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label' if selected and selected.title?
selected.title selected.title
else else
defaultLabel defaultLabel
......
...@@ -85,8 +85,10 @@ class @MergeRequestTabs ...@@ -85,8 +85,10 @@ class @MergeRequestTabs
scrollToElement: (container) -> scrollToElement: (container) ->
if window.location.hash if window.location.hash
$el = $("div#{container} #{window.location.hash}") navBarHeight = $('.navbar-gitlab').outerHeight()
$('body').scrollTo($el.offset().top) if $el.length
$el = $("#{container} #{window.location.hash}")
$.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length
# Activate a tab based on the current action # Activate a tab based on the current action
activateTab: (action) -> activateTab: (action) ->
...@@ -152,12 +154,38 @@ class @MergeRequestTabs ...@@ -152,12 +154,38 @@ class @MergeRequestTabs
@_get @_get
url: "#{source}.json" + @_location.search url: "#{source}.json" + @_location.search
success: (data) => success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html $('#diffs').html data.html
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
$('div#diffs .js-syntax-highlight').syntaxHighlight() $('#diffs .js-syntax-highlight').syntaxHighlight()
@expandViewContainer() if @diffViewType() is 'parallel' @expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true @diffsLoaded = true
@scrollToElement("#diffs") @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) -> loadBuilds: (source) ->
return if @buildsLoaded return if @buildsLoaded
......
...@@ -283,32 +283,10 @@ class @Notes ...@@ -283,32 +283,10 @@ class @Notes
show the form show the form
### ###
setupNoteForm: (form) -> setupNoteForm: (form) ->
disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") new GLForm form
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")
textarea = form.find(".js-note-text") 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, [ new Autosave textarea, [
"Note" "Note"
form.find("#note_commit_id").val() form.find("#note_commit_id").val()
...@@ -317,11 +295,6 @@ class @Notes ...@@ -317,11 +295,6 @@ class @Notes
form.find("#note_noteable_id").val() 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 Called in response to the new note form being submitted
...@@ -375,34 +348,15 @@ class @Notes ...@@ -375,34 +348,15 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.addClass "is-editting" note.addClass "is-editting"
form = note.find(".note-edit-form") form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)')
if isNewForm
form.addClass('gfm-form')
form.addClass('current-note-edit-form') form.addClass('current-note-edit-form')
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
# Setup markdown form new GLForm 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
if isNewForm form.find(".js-note-text").focus()
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
...@@ -559,6 +513,9 @@ class @Notes ...@@ -559,6 +513,9 @@ class @Notes
removeDiscussionNoteForm: (form)-> removeDiscussionNoteForm: (form)->
row = form.closest("tr") row = form.closest("tr")
glForm = form.data 'gl-form'
glForm.destroy()
form.find(".js-note-text").data("autosave").reset() form.find(".js-note-text").data("autosave").reset()
# show the reply button (will only work for replies) # show the reply button (will only work for replies)
...@@ -570,7 +527,6 @@ class @Notes ...@@ -570,7 +527,6 @@ class @Notes
# only remove the form # only remove the form
form.remove() form.remove()
cancelDiscussionForm: (e) => cancelDiscussionForm: (e) =>
e.preventDefault() e.preventDefault()
form = $(e.target).closest(".js-discussion-note-form") form = $(e.target).closest(".js-discussion-note-form")
......
...@@ -2,7 +2,7 @@ class @Subscription ...@@ -2,7 +2,7 @@ class @Subscription
constructor: (container) -> constructor: (container) ->
$container = $(container) $container = $(container)
@url = $container.attr('data-url') @url = $container.attr('data-url')
@subscribe_button = $container.find('.subscribe-button') @subscribe_button = $container.find('.js-subscribe-button')
@subscription_status = $container.find('.subscription-status') @subscription_status = $container.find('.subscription-status')
@subscribe_button.unbind('click').click(@toggleSubscription) @subscribe_button.unbind('click').click(@toggleSubscription)
......
/** /**
* Styles that apply to all GFM related forms. * 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 { .gfm-commit, .gfm-commit_range {
font-family: $monospace_font; font-family: $monospace_font;
......
.div-dropzone-wrapper { .div-dropzone-wrapper {
.div-dropzone { .div-dropzone {
position: relative; 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 { .div-dropzone-hover {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-top: -0.5em; margin-top: -11.5px;
margin-left: -0.6em; margin-left: -15px;
opacity: 0; opacity: 0;
font-size: 50px; font-size: 30px;
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out;
pointer-events: none; pointer-events: none;
} }
......
...@@ -43,7 +43,6 @@ ...@@ -43,7 +43,6 @@
@import "bootstrap/modals"; @import "bootstrap/modals";
@import "bootstrap/tooltip"; @import "bootstrap/tooltip";
@import "bootstrap/popovers"; @import "bootstrap/popovers";
@import "bootstrap/carousel";
// Utility classes // Utility classes
.clearfix { .clearfix {
......
...@@ -250,14 +250,6 @@ a > code { ...@@ -250,14 +250,6 @@ a > code {
* Textareas intended for GFM * Textareas intended for GFM
* *
*/ */
.js-gfm-input {
font-family: $monospace_font;
color: $gl-text-color;
}
.md-preview {
}
.strikethrough { .strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
......
...@@ -150,6 +150,7 @@ $light-grey-header: #faf9f9; ...@@ -150,6 +150,7 @@ $light-grey-header: #faf9f9;
*/ */
$gl-primary: $blue-normal; $gl-primary: $blue-normal;
$gl-success: $green-normal; $gl-success: $green-normal;
$gl-success-focus: rgba($gl-success, .4);
$gl-info: $blue-normal; $gl-info: $blue-normal;
$gl-warning: $orange-normal; $gl-warning: $orange-normal;
$gl-danger: $red-normal; $gl-danger: $red-normal;
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .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 { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .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 { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .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 { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .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 { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .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 { .diff-line-num {
&.old { &.old {
background-color: $line-number-old; background-color: $line-number-old;
......
...@@ -67,6 +67,24 @@ ...@@ -67,6 +67,24 @@
line-height: $code_line_height; line-height: $code_line_height;
font-size: $code_font_size; font-size: $code_font_size;
&.noteable_line {
position: relative;
&.old {
&:before {
content: '-';
position: absolute;
}
}
&.new {
&:before {
content: '+';
position: absolute;
}
}
}
span { span {
white-space: pre; white-space: pre;
} }
...@@ -391,3 +409,23 @@ ...@@ -391,3 +409,23 @@
margin-bottom: 0; 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);
}
}
}
}
...@@ -173,12 +173,6 @@ ...@@ -173,12 +173,6 @@
} }
} }
.subscribe-button {
span {
margin-top: 0;
}
}
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */ /* Extra small devices (phones, less than 768px) */
display: none; display: none;
...@@ -322,3 +316,9 @@ ...@@ -322,3 +316,9 @@
color: #8c8c8c; color: #8c8c8c;
} }
} }
.issuable-form-padding-top {
@media (min-width: $screen-sm-min) {
padding-top: 7px;
}
}
...@@ -79,19 +79,30 @@ ...@@ -79,19 +79,30 @@
color: $white-light; 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 { .manage-labels-list {
.prepend-left-10 { .prepend-left-10, .prepend-description-left {
display: inline-block; display: inline-block;
width: 40%; width: 40%;
vertical-align: middle; vertical-align: middle;
@media (max-width: $screen-xs-min) { @include labels-mobile;
display: block; }
width: 100%;
margin-left: 0; .prepend-description-left {
padding: 10px 0; width: 57%;
}
@include labels-mobile;
} }
.pull-info-right { .pull-info-right {
...@@ -106,7 +117,7 @@ ...@@ -106,7 +117,7 @@
padding: 6px; padding: 6px;
color: $gl-text-color; color: $gl-text-color;
&.subscribe-button { &.label-subscribe-button {
padding-left: 0; padding-left: 0;
} }
} }
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
} }
.note-textarea { .note-textarea {
display: block;
padding: 10px 0; padding: 10px 0;
font-family: $regular_font; font-family: $regular_font;
border: 0; border: 0;
...@@ -63,7 +64,7 @@ ...@@ -63,7 +64,7 @@
&.is-focused { &.is-focused {
border-color: $focus-border-color; 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); 0 0 4px rgba($focus-border-color, .4);
.comment-toolbar, .comment-toolbar,
...@@ -72,6 +73,17 @@ ...@@ -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 { p {
code { code {
white-space: normal; white-space: normal;
......
...@@ -276,8 +276,7 @@ ul.notes { ...@@ -276,8 +276,7 @@ ul.notes {
.diff-file tr.line_holder { .diff-file tr.line_holder {
@mixin show-add-diff-note { @mixin show-add-diff-note {
filter: alpha(opacity=100); display: inline-block;
opacity: 1.0;
} }
.add-diff-note { .add-diff-note {
...@@ -291,13 +290,8 @@ ul.notes { ...@@ -291,13 +290,8 @@ ul.notes {
position: absolute; position: absolute;
z-index: 10; z-index: 10;
width: 32px; width: 32px;
transition: all 0.2s ease;
// "hide" it by default // "hide" it by default
opacity: 0.0; display: none;
filter: alpha(opacity=0);
&:hover { &:hover {
background: $gl-info; background: $gl-info;
color: #fff; color: #fff;
......
...@@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to admin_runners_path redirect_to admin_runners_path
end 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 private
def set_application_setting def set_application_setting
...@@ -82,6 +91,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -82,6 +91,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_enabled, :akismet_enabled,
:akismet_api_key, :akismet_api_key,
:email_author_in_body, :email_author_in_body,
:repository_checks_enabled,
restricted_visibility_levels: [], restricted_visibility_levels: [],
import_sources: [] import_sources: []
) )
......
class Admin::ProjectsController < Admin::ApplicationController 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] before_action :group, only: [:show, :transfer]
def index def index
...@@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -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.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.with_push if params[:with_push].present? @projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].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.non_archived unless params[:with_archived].present?
@projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
...@@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController
redirect_to admin_namespace_project_path(@project.namespace, @project) redirect_to admin_namespace_project_path(@project.namespace, @project)
end 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 protected
def project def project
......
...@@ -3,6 +3,7 @@ require 'fogbugz' ...@@ -3,6 +3,7 @@ require 'fogbugz'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Gitlab::GonHelper
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
...@@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base ...@@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base
before_action :check_password_expiration before_action :check_password_expiration
before_action :check_2fa_requirement before_action :check_2fa_requirement
before_action :ldap_security_check before_action :ldap_security_check
before_action :sentry_user_context before_action :sentry_context
before_action :default_headers before_action :default_headers
before_action :add_gon_variables before_action :add_gon_variables
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
...@@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base ...@@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base
protected protected
def sentry_user_context def sentry_context
if Rails.env.production? && current_application_settings.sentry_enabled && current_user if Rails.env.production? && current_application_settings.sentry_enabled
Raven.user_context( if current_user
id: current_user.id, Raven.user_context(
email: current_user.email, id: current_user.id,
username: current_user.username, email: current_user.email,
) username: current_user.username,
)
end
Raven.tags_context(program: sentry_program_context) Raven.tags_context(program: sentry_program_context)
end end
...@@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base ...@@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base
end end
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! def validate_user_service_ticket!
return unless signed_in? && session[:service_tickets] return unless signed_in? && session[:service_tickets]
......
...@@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController ...@@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController
@last_push = current_user.recent_push if current_user @last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank? @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
......
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper include PageLayoutHelper
before_action :verify_user_oauth_applications_enabled before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user! before_action :authenticate_user!
before_action :add_gon_variables
layout 'profile' layout 'profile'
......
...@@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions include IssuableActions
before_action :module_enabled 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 # Allow read any issue
before_action :authorize_read_issue!, only: [:show] before_action :authorize_read_issue!, only: [:show]
...@@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow issues bulk update # Allow issues bulk update
before_action :authorize_admin_issues!, only: [: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 respond_to :html
def index def index
...@@ -65,8 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -65,8 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue @noteable = @issue
@merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches(current_user)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -118,15 +114,38 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -118,15 +114,38 @@ class Projects::IssuesController < Projects::ApplicationController
end end
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(current_user)
respond_to do |format|
format.json do
render json: {
html: view_to_html_string('projects/issues/_related_branches')
}
end
end
end
def bulk_update def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end end
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
protected protected
def issue def issue
......
...@@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController ...@@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
end end
def archive def archive
RepositoryArchiveCacheWorker.perform_async
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
head :ok head :ok
rescue => ex rescue => ex
......
...@@ -40,10 +40,11 @@ module DiffHelper ...@@ -40,10 +40,11 @@ module DiffHelper
(unfold) ? 'unfold js-unfold' : '' (unfold) ? 'unfold js-unfold' : ''
end end
def diff_line_content(line) def diff_line_content(line, line_type = nil)
if line.blank? if line.blank?
" &nbsp;".html_safe " &nbsp;".html_safe
else else
line[0] = ' ' if %w[new old].include?(line_type)
line line
end end
end end
......
class RepositoryCheckMailer < BaseMailer
def notify(failed_count)
if failed_count == 1
@message = "One project failed its last repository check"
else
@message = "#{failed_count} projects failed their last repository check"
end
mail(
to: User.admins.pluck(:email),
subject: @message
)
end
end
...@@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
require_two_factor_authentication: false, require_two_factor_authentication: false,
two_factor_grace_period: 48, two_factor_grace_period: 48,
recaptcha_enabled: false, recaptcha_enabled: false,
akismet_enabled: false akismet_enabled: false,
repository_checks_enabled: true,
) )
end end
......
...@@ -150,13 +150,11 @@ class Commit ...@@ -150,13 +150,11 @@ class Commit
end end
def hook_attrs(with_changed_files: false) def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace
data = { data = {
id: id, id: id,
message: safe_message, message: safe_message,
timestamp: committed_date.xmlschema, timestamp: committed_date.xmlschema,
url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}", url: Gitlab::UrlBuilder.build(self),
author: { author: {
name: author_name, name: author_name,
email: author_email email: author_email
......
...@@ -105,11 +105,10 @@ class Issue < ActiveRecord::Base ...@@ -105,11 +105,10 @@ class Issue < ActiveRecord::Base
end end
# All branches containing the current issue's ID, except for # All branches containing the current issue's ID, except for
# those with a merge request open (that the current user can see) # those with a merge request open referencing the current issue.
# referencing the current issue.
def related_branches(current_user) def related_branches(current_user)
branches_with_iid = project.repository.branch_names.select do |branch| branches_with_iid = project.repository.branch_names.select do |branch|
branch.end_with?("-#{iid}") branch =~ /\A#{iid}-(?!\d+-stable)/i
end end
branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch) branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
...@@ -161,7 +160,7 @@ class Issue < ActiveRecord::Base ...@@ -161,7 +160,7 @@ class Issue < ActiveRecord::Base
if self.confidential? if self.confidential?
"issue-#{iid}" "issue-#{iid}"
else else
"#{title.parameterize}-#{iid}" "#{iid}-#{title.parameterize}"
end end
end end
......
...@@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base
validates :target_project, presence: true validates :target_project, presence: true
validates :target_branch, presence: true validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds? validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches validate :validate_branches, unless: :allow_broken
validate :validate_fork validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
...@@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base
end end
if opened? || reopened? if opened? || reopened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any? if similar_mrs.any?
errors.add :validate_branches, errors.add :validate_branches,
...@@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base
def hook_attrs def hook_attrs
attrs = { attrs = {
source: source_project.hook_attrs, source: source_project.try(:hook_attrs),
target: target_project.hook_attrs, target: target_project.hook_attrs,
last_commit: nil, last_commit: nil,
work_in_progress: work_in_progress? work_in_progress: work_in_progress?
......
# == 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
...@@ -82,17 +82,17 @@ class BambooService < CiService ...@@ -82,17 +82,17 @@ class BambooService < CiService
end end
def build_info(sha) 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? if username.blank? && password.blank?
@response = HTTParty.get(parsed_url.to_s, verify: false) @response = HTTParty.get(url, verify: false)
else else
get_url = "#{url}&os_authType=basic" url << '&os_authType=basic'
auth = { auth = {
username: username, username: username,
password: password, password: password
} }
@response = HTTParty.get(get_url, verify: false, basic_auth: auth) @response = HTTParty.get(url, verify: false, basic_auth: auth)
end end
end end
...@@ -101,11 +101,11 @@ class BambooService < CiService ...@@ -101,11 +101,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0' if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page. # 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 else
# If actual build link is available, go to build result page. # If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key'] result_key = @response['results']['results']['result']['planResultKey']['key']
"#{bamboo_url}/browse/#{result_key}" URI.join(bamboo_url, "/browse/#{result_key}").to_s
end end
end end
...@@ -134,7 +134,7 @@ class BambooService < CiService ...@@ -134,7 +134,7 @@ class BambooService < CiService
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
# Bamboo requires a GET and does not take any data. # Bamboo requires a GET and does not take any data.
self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
verify: false) self.class.get(url, verify: false)
end end
end end
...@@ -23,7 +23,7 @@ class BuildsEmailService < Service ...@@ -23,7 +23,7 @@ class BuildsEmailService < Service
prop_accessor :recipients prop_accessor :recipients
boolean_accessor :add_pusher boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_builds 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 def initialize_properties
if properties.nil? if properties.nil?
...@@ -87,10 +87,14 @@ class BuildsEmailService < Service ...@@ -87,10 +87,14 @@ class BuildsEmailService < Service
end end
def all_recipients(data) 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] if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}" all_recipients << data[:user][:email]
end end
all_recipients all_recipients
......
...@@ -85,13 +85,15 @@ class TeamcityService < CiService ...@@ -85,13 +85,15 @@ class TeamcityService < CiService
end end
def build_info(sha) def build_info(sha)
url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ url = URI.join(
"branch:unspecified:any,number:#{sha}") teamcity_url,
"/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
).to_s
auth = { auth = {
username: username, 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 end
def build_page(sha, ref) def build_page(sha, ref)
...@@ -100,12 +102,14 @@ class TeamcityService < CiService ...@@ -100,12 +102,14 @@ class TeamcityService < CiService
if @response.code != 200 if @response.code != 200
# If actual build link can't be determined, # If actual build link can't be determined,
# send user to build summary page. # 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 else
# If actual build link is available, go to build result page. # If actual build link is available, go to build result page.
built_id = @response['build']['id'] built_id = @response['build']['id']
"#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ URI.join(
"&buildTypeId=#{build_type}" teamcity_url,
"/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
).to_s
end end
end end
...@@ -140,12 +144,13 @@ class TeamcityService < CiService ...@@ -140,12 +144,13 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref]) branch = Gitlab::Git.ref_name(data[:ref])
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", self.class.post(
body: "<build branchName=\"#{branch}\">"\ URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
"<buildType id=\"#{build_type}\"/>"\ body: "<build branchName=\"#{branch}\">"\
'</build>', "<buildType id=\"#{build_type}\"/>"\
headers: { 'Content-type' => 'application/xml' }, '</build>',
basic_auth: auth headers: { 'Content-type' => 'application/xml' },
) basic_auth: auth
)
end end
end end
...@@ -253,6 +253,8 @@ class Repository ...@@ -253,6 +253,8 @@ class Repository
# This ensures this particular cache is flushed after the first commit to a # This ensures this particular cache is flushed after the first commit to a
# new repository. # new repository.
expire_emptiness_caches if empty? expire_emptiness_caches if empty?
expire_branch_count_cache
expire_tag_count_cache
end end
def expire_branch_cache(branch_name = nil) def expire_branch_cache(branch_name = nil)
......
...@@ -3,7 +3,7 @@ module Issues ...@@ -3,7 +3,7 @@ module Issues
def hook_data(issue, action) def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user) 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[:object_attributes].merge!(url: issue_url, action: action)
issue_data issue_data
end end
......
...@@ -20,8 +20,7 @@ module MergeRequests ...@@ -20,8 +20,7 @@ module MergeRequests
def hook_data(merge_request, action) def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user) 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] = Gitlab::UrlBuilder.build(merge_request)
hook_data[:object_attributes][:url] = merge_request_url
hook_data[:object_attributes][:action] = action hook_data[:object_attributes][:action] = action
hook_data hook_data
end end
......
...@@ -51,7 +51,7 @@ module MergeRequests ...@@ -51,7 +51,7 @@ module MergeRequests
# be interpreted as the use wants to close that issue on this project # be interpreted as the use wants to close that issue on this project
# Pattern example: 112-fix-mep-mep # Pattern example: 112-fix-mep-mep
# Will lead to appending `Closes #112` to the description # 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] iid = match[1]
closes_issue = "Closes ##{iid}" closes_issue = "Closes ##{iid}"
......
...@@ -26,7 +26,9 @@ module Projects ...@@ -26,7 +26,9 @@ module Projects
GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
ensure ensure
@project.update_column(:pushes_since_gc, 0) Gitlab::Metrics.measure(:reset_pushes_since_gc) do
@project.update_column(:pushes_since_gc, 0)
end
end end
def needed? def needed?
...@@ -34,14 +36,18 @@ module Projects ...@@ -34,14 +36,18 @@ module Projects
end end
def increment! def increment!
@project.increment!(:pushes_since_gc) Gitlab::Metrics.measure(:increment_pushes_since_gc) do
@project.increment!(:pushes_since_gc)
end
end end
private private
def try_obtain_lease def try_obtain_lease
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease.try_obtain lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end end
end end
end end
...@@ -34,8 +34,9 @@ module Projects ...@@ -34,8 +34,9 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists") raise TransferError.new("Project with same path in target namespace already exists")
end end
# Apply new namespace id # Apply new namespace id and visibility level
project.namespace = new_namespace project.namespace = new_namespace
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save! project.save!
# Notifications # Notifications
...@@ -56,7 +57,7 @@ module Projects ...@@ -56,7 +57,7 @@ module Projects
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
project.old_path_with_namespace = old_path project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer) SystemHooksService.new.execute_hooks_for(project, :transfer)
true true
end end
......
...@@ -222,7 +222,7 @@ class SystemNoteService ...@@ -222,7 +222,7 @@ class SystemNoteService
# Called when a branch is created from the 'new branch' button on a issue # Called when a branch is created from the 'new branch' button on a issue
# Example note text: # Example note text:
# #
# "Started branch `issue-branch-button-201`" # "Started branch `201-issue-branch-button`"
def self.new_issue_branch(issue, project, author, branch) def self.new_issue_branch(issue, project, author, branch)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
......
...@@ -271,5 +271,24 @@ ...@@ -271,5 +271,24 @@
.col-sm-10 .col-sm-10
= f.text_field :sentry_dsn, class: 'form-control' = 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 .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
- page_title "Logs" - page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger] Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger]
%ul.nav-links.log-tabs %ul.nav-links.log-tabs
- loggers.each do |klass| - loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.row.prepend-top-default .row.prepend-top-default
%aside.col-md-3 %aside.col-md-3
.admin-filter .panel.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do = form_tag admin_namespaces_projects_path, method: :get, class: '' do
.form-group .form-group
= label_tag :name, 'Name:' = label_tag :name, 'Name:'
...@@ -38,7 +38,13 @@ ...@@ -38,7 +38,13 @@
%span.descr %span.descr
= visibility_level_icon(level) = visibility_level_icon(level)
= label = 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] = hidden_field_tag :sort, params[:sort]
= button_tag "Search", class: "btn submit btn-primary" = button_tag "Search", class: "btn submit btn-primary"
= link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
......
...@@ -5,6 +5,16 @@ ...@@ -5,6 +5,16 @@
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
Edit Edit
%hr %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 .row
.col-md-6 .col-md-6
.panel.panel-default .panel.panel-default
...@@ -95,6 +105,32 @@ ...@@ -95,6 +105,32 @@
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
= f.submit 'Transfer', class: 'btn btn-primary' = 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 .col-md-6
- if @group - if @group
.panel.panel-default .panel.panel-default
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
%td= app.name %td= app.name
%td= token.created_at %td= token.created_at
%td= token.scopes %td= token.scopes
%td= render 'delete_form', application: app %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- @authorized_anonymous_tokens.each do |token| - @authorized_anonymous_tokens.each do |token|
%tr %tr
%td %td
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
This will create milestone in every selected project This will create milestone in every selected project
%hr %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 .row
- if @milestone.errors.any? - if @milestone.errors.any?
#error_explanation #error_explanation
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix .clearfix
.error-alert .error-alert
.form-group .form-group
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render 'layouts/search' = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm.visible-xs %li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
......
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - 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 - 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: "#" } %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress') = icon('compress')
- if @lines.present? - if @lines.present?
- if @form.unfold? && @form.since != 1 && !@form.bottom? - if @form.unfold? && @form.since != 1 && !@form.bottom?
%tr.line_holder{ id: @form.since } %tr.line_holder{ id: @form.since }
= render "projects/diffs/match_line", {line: @match_line, = render "projects/diffs/match_line", { line: @match_line,
line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
- @lines.each_with_index do |line, index| - @lines.each_with_index do |line, index|
- line_new = index + @form.since - line_new = index + @form.since
- line_old = line_new - @form.offset - line_old = line_new - @form.offset
%tr.line_holder %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), "#" = 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) , "#" = link_to raw(line_new) , "#"
%td.line_content.noteable_line==#{' ' * @form.indent}#{line} %td.line_content.noteable_line==#{' ' * @form.indent}#{line}
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc - if @form.unfold? && @form.bottom? && @form.to < @blob.loc
%tr.line_holder{ id: @form.to } %tr.line_holder{ id: @form.to }
= render "projects/diffs/match_line", {line: @match_line, = render "projects/diffs/match_line", { line: @match_line,
line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} line_old: @form.to, line_new: @form.to, bottom: true, new_file: false }
- type = line.type - type = line.type
%tr.line_holder{id: line_code, class: type} %tr.line_holder{ id: line_code, class: type }
- case type - case type
- when 'match' - when 'match'
= render "projects/diffs/match_line", {line: line.text, = 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} line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file }
- when 'nonewline' - when 'nonewline'
%td.old_line.diff-line-num %td.old_line.diff-line-num
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.match= line.text %td.line_content.match= line.text
- else - else
%td.old_line.diff-line-num{class: type} %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = raw(type == "new" ? "&nbsp;" : line.old_pos) - link_text = type == "new" ? "&nbsp;".html_safe : line.old_pos
- if defined?(plain) && plain - if defined?(plain) && plain
= link_text = link_text
- else - 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) - if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(line_code) = link_to_new_diff_note(line_code)
%td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}} %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = raw(type == "old" ? "&nbsp;" : line.new_pos) - link_text = type == "old" ? "&nbsp;".html_safe : line.new_pos
- if defined?(plain) && plain - if defined?(plain) && plain
= link_text = link_text
- else - else
= link_to link_text, "##{line_code}", id: line_code = 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) %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type)
...@@ -14,11 +14,11 @@ ...@@ -14,11 +14,11 @@
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.parallel.match= left[:text] %td.line_content.parallel.match= left[:text]
- else - 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] = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- if @comments_allowed && can?(current_user, :create_note, @project) - if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(left[:line_code], 'old') = 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' - if right[:type] == 'new'
- new_line_class = 'new' - new_line_class = 'new'
...@@ -27,11 +27,11 @@ ...@@ -27,11 +27,11 @@
- new_line_class = nil - new_line_class = nil
- new_line_code = left[:line_code] - 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 = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- if @comments_allowed && can?(current_user, :create_note, @project) - if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(right[:line_code], 'new') = 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 - if @reply_allowed
- comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
......
...@@ -210,6 +210,7 @@ ...@@ -210,6 +210,7 @@
%li Be careful. Changing the project's namespace can have unintended side effects. %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 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 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 .form-actions
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- else - else
......
= 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 = render 'shared/issuable/form', f: f, issuable: @issue
:javascript :javascript
......
...@@ -64,9 +64,11 @@ ...@@ -64,9 +64,11 @@
= @issue.description = @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
.merge-requests #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)}
= render 'merge_requests' // This element is filled in using JavaScript.
= render 'related_branches'
#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 .content-block.content-block-small
= render 'new_branch' = render 'new_branch'
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
.subscription-status{data: {status: label_subscription_status(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) %span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project - if can? current_user, :admin_label, @project
......
= 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 = render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript :javascript
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%span.pull-right %span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request) = link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr %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 = render 'shared/issuable/form', f: f, issuable: @merge_request
= f.hidden_field :source_project_id = f.hidden_field :source_project_id
= f.hidden_field :source_branch = f.hidden_field :source_branch
......
= 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) = form_errors(@milestone)
.row .row
.col-md-6 .col-md-6
.form-group .form-group
...@@ -11,7 +10,7 @@ ...@@ -11,7 +10,7 @@
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -2,7 +2,7 @@ ...@@ -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| = 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) = note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-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' = render 'projects/notes/hints'
.note-form-actions.clearfix .note-form-actions.clearfix
......
= 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 :view, diff_view
= hidden_field_tag :line_type = hidden_field_tag :line_type
= note_target_fields(@note) = note_target_fields(@note)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = 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' = render 'projects/notes/hints'
.error-alert .error-alert
......
...@@ -20,11 +20,9 @@ ...@@ -20,11 +20,9 @@
%td.new_line.diff-line-num= "..." %td.new_line.diff-line-num= "..."
%td.line_content.match= line.text %td.line_content.match= line.text
- else - else
%td.old_line.diff-line-num %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? "&nbsp;".html_safe : line.old_pos } }
= raw(type == "new" ? "&nbsp;" : line.old_pos) %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? "&nbsp;".html_safe : line.new_pos } }
%td.new_line.diff-line-num %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type)
= 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)
- if line_code == note.line_code - if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes = render "projects/notes/diff_notes_with_reply", notes: discussion_notes
...@@ -9,11 +9,11 @@ ...@@ -9,11 +9,11 @@
%strong #{@tag.name} %strong #{@tag.name}
.prepend-top-default .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 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' = render 'projects/notes/hints'
.error-alert .error-alert
.form-actions.prepend-top-default .form-actions.prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save' = 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" = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
New Tag New Tag
%hr %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 .form-group
= label_tag :tag_name, nil, class: 'control-label' = label_tag :tag_name, nil, class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = 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' = 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 .form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel'
......
= 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) = form_errors(@page)
= f.hidden_field :title, value: @page.title = f.hidden_field :title, value: @page.title
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= f.label :content, class: 'control-label' = f.label :content, class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :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' = render 'projects/notes/hints'
.clearfix .clearfix
......
%p
#{@message}.
%p
= link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
#{@message}.
\
View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
- project = note.project - 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 - noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row .search-result-row
%h5.note-search-caption.str-truncated %h5.note-search-caption.str-truncated
......
...@@ -29,7 +29,8 @@ ...@@ -29,7 +29,8 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, = 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' = render 'projects/notes/hints'
.clearfix .clearfix
.error-alert .error-alert
...@@ -70,13 +71,13 @@ ...@@ -70,13 +71,13 @@
- if can? current_user, :admin_milestone, issuable.project - 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 = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank
.form-group .form-group
- has_labels = issuable.project.labels.any?
= f.label :label_ids, "Labels", class: 'control-label' = f.label :label_ids, "Labels", class: 'control-label'
.col-sm-10 .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
- if issuable.project.labels.any? - if has_labels
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name, = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
{ selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else - else
.prepend-top-10
%span.light No labels yet. %span.light No labels yet.
&nbsp; &nbsp;
- if can? current_user, :admin_label, issuable.project - if can? current_user, :admin_label, issuable.project
...@@ -128,8 +129,6 @@ ...@@ -128,8 +129,6 @@
- else - else
.pull-right .pull-right
- if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
= link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, = 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-grouped' do method: :delete, class: 'btn btn-danger btn-grouped'
= icon('trash-o')
Delete
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
- if params[:label_name].present? - if params[:label_name].present?
= hidden_field_tag(:label_name, params[:label_name]) = hidden_field_tag(:label_name, params[:label_name])
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
%span.dropdown-toggle-text %span.dropdown-toggle-text
= h(params[:label_name].presence || "Label") = h(params[:label_name].presence || "Label")
= icon('chevron-down') = icon('chevron-down')
......
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
.title.hide-collapsed .title.hide-collapsed
Notifications Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - 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' %span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status.hide-collapsed{data: {status: subscribtion_status}} .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )} .unsubscribed{class: ( 'hidden' if subscribed )}
...@@ -152,4 +152,4 @@ ...@@ -152,4 +152,4 @@
new LabelsSelect(); new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription') new Subscription('.subscription')
new Sidebar(); new Sidebar();
\ No newline at end of file
...@@ -4,15 +4,16 @@ ...@@ -4,15 +4,16 @@
%li %li
%span.label-row %span.label-row
= link_to milestones_label_path(options) do %span.label-name
- render_colored_label(label, tooltip: false) = link_to milestones_label_path(options) do
%span.prepend-left-10 - render_colored_label(label, tooltip: false)
%span.prepend-description-left
= markdown(label.description, pipeline: :single_line) = markdown(label.description, pipeline: :single_line)
.pull-right .pull-info-right
%strong.issues-count %span.append-right-20
= link_to milestones_label_path(options.merge(state: 'opened')) do = link_to milestones_label_path(options.merge(state: 'opened')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - 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 = link_to milestones_label_path(options.merge(state: 'closed')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
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
...@@ -40,7 +40,7 @@ class PostReceive ...@@ -40,7 +40,7 @@ class PostReceive
if Gitlab::Git.tag_ref?(ref) if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, 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 GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end end
end end
......
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
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
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
...@@ -18,7 +18,7 @@ Dir.chdir APP_ROOT do ...@@ -18,7 +18,7 @@ Dir.chdir APP_ROOT do
# end # end
puts "\n== Preparing database ==" puts "\n== Preparing database =="
system "bin/rake db:setup" system "bin/rake db:reset"
puts "\n== Removing old logs and tempfiles ==" puts "\n== Removing old logs and tempfiles =="
system "rm -f log/*" system "rm -f log/*"
......
...@@ -46,6 +46,15 @@ production: &base ...@@ -46,6 +46,15 @@ production: &base
# #
# relative_url_root: /gitlab # relative_url_root: /gitlab
# Trusted Proxies
# Customize if you have GitLab behind a reverse proxy which is running on a different machine.
# Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address.
trusted_proxies:
# Examples:
#- 192.168.1.0/24
#- 192.168.2.1
#- 2001:0db8::/32
# Uncomment and customize if you can't use the default user to run GitLab (default: 'git') # Uncomment and customize if you can't use the default user to run GitLab (default: 'git')
# user: git # user: git
...@@ -155,7 +164,17 @@ production: &base ...@@ -155,7 +164,17 @@ production: &base
# Flag stuck CI builds as failed # Flag stuck CI builds as failed
stuck_ci_builds_worker: stuck_ci_builds_worker:
cron: "0 0 * * *" 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:
cron: "0 * * * *"
# #
# 2. GitLab CI settings # 2. GitLab CI settings
...@@ -304,6 +323,13 @@ production: &base ...@@ -304,6 +323,13 @@ production: &base
# (default: false) # (default: false)
auto_link_saml_user: false auto_link_saml_user: false
# Set different Omniauth providers as external so that all users creating accounts
# via these providers will not be able to have access to internal projects. You
# will need to use the full name of the provider, like `google_oauth2` for Google.
# Refer to the examples below for the full names of the supported providers.
# (default: [])
external_providers: []
## Auth providers ## Auth providers
# Uncomment the following lines and fill in the data of the auth provider you want to use # Uncomment the following lines and fill in the data of the auth provider you want to use
# If your favorite auth provider is not listed you can use others: # If your favorite auth provider is not listed you can use others:
......
...@@ -129,6 +129,7 @@ Settings['omniauth'] ||= Settingslogic.new({}) ...@@ -129,6 +129,7 @@ Settings['omniauth'] ||= Settingslogic.new({})
Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil?
Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil?
Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil?
Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil?
Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil?
Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil?
Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil?
...@@ -190,6 +191,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send ...@@ -190,6 +191,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['restricted_signup_domains'] ||= []
Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
Settings.gitlab['trusted_proxies'] ||= []
# #
...@@ -239,7 +241,15 @@ Settings['cron_jobs'] ||= Settingslogic.new({}) ...@@ -239,7 +241,15 @@ Settings['cron_jobs'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker'] ||= 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']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' 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 # GitLab Shell
......
...@@ -14,7 +14,7 @@ if Rails.env.test? ...@@ -14,7 +14,7 @@ if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else else
redis_config = Gitlab::Redis.redis_store_options redis_config = Gitlab::Redis.redis_store_options
redis_config[:namespace] = 'session:gitlab' redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE
Gitlab::Application.config.session_store( Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks. :redis_store, # Using the cookie_store would enable session replay attacks.
......
SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = { config.redis = {
url: Gitlab::Redis.url, url: Gitlab::Redis.url,
namespace: SIDEKIQ_REDIS_NAMESPACE namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
} }
config.server_middleware do |chain| config.server_middleware do |chain|
...@@ -30,6 +28,6 @@ end ...@@ -30,6 +28,6 @@ end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = { config.redis = {
url: Gitlab::Redis.url, url: Gitlab::Redis.url,
namespace: SIDEKIQ_REDIS_NAMESPACE namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
} }
end end
Rails.application.config.action_dispatch.trusted_proxies =
[ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies)
...@@ -264,6 +264,7 @@ Rails.application.routes.draw do ...@@ -264,6 +264,7 @@ Rails.application.routes.draw do
member do member do
put :transfer put :transfer
post :repository_check
end end
resources :runner_projects resources :runner_projects
...@@ -281,6 +282,7 @@ Rails.application.routes.draw do ...@@ -281,6 +282,7 @@ Rails.application.routes.draw do
resource :application_settings, only: [:show, :update] do resource :application_settings, only: [:show, :update] do
resources :services resources :services
put :reset_runners_token put :reset_runners_token
put :clear_repository_check_states
end end
resources :labels resources :labels
...@@ -701,6 +703,8 @@ Rails.application.routes.draw do ...@@ -701,6 +703,8 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do resources :issues, constraints: { id: /\d+/ } do
member do member do
post :toggle_subscription post :toggle_subscription
get :referenced_merge_requests
get :related_branches
end end
collection do collection do
post :bulk_update post :bulk_update
......
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
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
# #
class MigrateNewNotificationSetting < ActiveRecord::Migration class MigrateNewNotificationSetting < ActiveRecord::Migration
def up 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" 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 end
......
class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration
def change
add_column :application_settings, :repository_checks_enabled, :boolean, default: true
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160331223143) do ActiveRecord::Schema.define(version: 20160412140240) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -77,6 +77,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do ...@@ -77,6 +77,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do
t.string "akismet_api_key" t.string "akismet_api_key"
t.boolean "email_author_in_body", default: false t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility" t.integer "default_group_visibility"
t.boolean "repository_checks_enabled", default: true
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
...@@ -743,6 +744,8 @@ ActiveRecord::Schema.define(version: 20160331223143) do ...@@ -743,6 +744,8 @@ ActiveRecord::Schema.define(version: 20160331223143) do
t.boolean "public_builds", default: true, null: false t.boolean "public_builds", default: true, null: false
t.string "main_language" t.string "main_language"
t.integer "pushes_since_gc", default: 0 t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
end end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
...@@ -752,6 +755,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do ...@@ -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", ["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", ["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_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", ["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", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
## User documentation ## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API. - [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples. - [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md). - [Importing to GitLab](workflow/importing/README.md).
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
- [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running - [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects. - [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. - [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. - [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. - [Update](update/README.md) Update guides to upgrade your installation.
......
# 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
...@@ -108,6 +108,7 @@ The following table shows the possible return codes for API requests. ...@@ -108,6 +108,7 @@ The following table shows the possible return codes for API requests.
| ------------- | ----------- | | ------------- | ----------- |
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | | `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | | `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
......
...@@ -126,6 +126,87 @@ Parameters: ...@@ -126,6 +126,87 @@ Parameters:
- `id` (required) - The ID or path of a group - `id` (required) - The ID or path of a group
- `project_id` (required) - The ID of a project - `project_id` (required) - The ID of a project
## Update group
Updates the project group. Only available to group owners and administrators.
```
PUT /groups/:id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the group |
| `name` | string | no | The name of the group |
| `path` | string | no | The path of the group |
| `description` | string | no | The description of the group |
| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
```bash
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
```
Example response:
```json
{
"id": 5,
"name": "Experimental",
"path": "h5bp",
"description": "foo",
"visibility_level": 10,
"avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp",
"projects": [
{
"id": 9,
"description": "foo",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 10,
"ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
"http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
"web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
"name": "Html5 Boilerplate",
"name_with_namespace": "Experimental / Html5 Boilerplate",
"path": "html5-boilerplate",
"path_with_namespace": "h5bp/html5-boilerplate",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": true,
"created_at": "2016-04-05T21:40:50.169Z",
"last_activity_at": "2016-04-06T16:52:08.432Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 5,
"name": "Experimental",
"path": "h5bp",
"owner_id": null,
"created_at": "2016-04-05T21:40:49.152Z",
"updated_at": "2016-04-07T08:07:48.466Z",
"description": "foo",
"avatar": {
"url": null
},
"share_with_group_lock": false,
"visibility_level": 10
},
"avatar_url": null,
"star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true
}
]
}
```
## Remove group ## Remove group
Removes group with all projects inside. Removes group with all projects inside.
......
...@@ -298,6 +298,7 @@ PUT /projects/:id/issues/:issue_id ...@@ -298,6 +298,7 @@ PUT /projects/:id/issues/:issue_id
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue | | `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 | | `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 ```bash
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
...@@ -351,6 +352,170 @@ DELETE /projects/:id/issues/:issue_id ...@@ -351,6 +352,170 @@ DELETE /projects/:id/issues/:issue_id
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
``` ```
## Move an issue
Moves an issue to a different project. If the operation is successful, a status
code `201` together with moved issue is returned. If the project, issue, or
target project is not found, error `404` is returned. If the target project
equals the source project or the user has insufficient permissions to move an
issue, error `400` together with an explaining error message is returned.
```
POST /projects/:id/issues/:issue_id/move
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `issue_id` | integer | yes | The ID of a project's issue |
| `to_project_id` | integer | yes | The ID of the new project |
```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
```
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"
}
}
```
## 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 on issues
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
...@@ -606,3 +606,151 @@ Example response: ...@@ -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
}
```
...@@ -89,6 +89,7 @@ Parameters: ...@@ -89,6 +89,7 @@ Parameters:
- `id` (required) - The ID of a project - `id` (required) - The ID of a project
- `issue_id` (required) - The ID of an issue - `issue_id` (required) - The ID of an issue
- `body` (required) - The content of a note - `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 ### Modify existing issue note
......
...@@ -491,6 +491,132 @@ Parameters: ...@@ -491,6 +491,132 @@ Parameters:
- `id` (required) - The ID of the project to be forked - `id` (required) - The ID of the project to be forked
### Star a project
Stars a given project. Returns status code `201` and the project on success and
`304` if the project is already starred.
```
POST /projects/:id/star
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project |
```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
```
Example response:
```json
{
"id": 3,
"description": null,
"default_branch": "master",
"public": false,
"visibility_level": 10,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
"tag_list": [
"example",
"disapora project"
],
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
"creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
"id": 3,
"name": "Diaspora",
"owner_id": 1,
"path": "diaspora",
"updated_at": "2013-09-30T13: 46: 02Z"
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 1
}
```
### Unstar a project
Unstars a given project. Returns status code `200` and the project on success
and `304` if the project is not starred.
```
DELETE /projects/:id/star
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project |
```bash
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
```
Example response:
```json
{
"id": 3,
"description": null,
"default_branch": "master",
"public": false,
"visibility_level": 10,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
"tag_list": [
"example",
"disapora project"
],
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
"creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
"id": 3,
"name": "Diaspora",
"owner_id": 1,
"path": "diaspora",
"updated_at": "2013-09-30T13: 46: 02Z"
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0
}
```
### Archive a project ### Archive a project
Archives the project if the user is either admin or the project owner of this project. This action is Archives the project if the user is either admin or the project owner of this project. This action is
......
...@@ -19,7 +19,7 @@ many projects, you can have a single or a small number of runners that handle ...@@ -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. multiple projects. This makes it easier to maintain and update runners.
**Specific runners** are useful for jobs that have special requirements or for **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 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 runners. For example, if you want to deploy a certain project, you can setup
a specific runner to have the right credentials for this. a specific runner to have the right credentials for this.
......
...@@ -30,7 +30,7 @@ This is the universal solution which works with any type of executor ...@@ -30,7 +30,7 @@ This is the universal solution which works with any type of executor
## SSH keys when using the Docker executor ## SSH keys when using the Docker executor
You will first need to create an SSH key pair. For more information, follow the You will first need to create an SSH key pair. For more information, follow the
instructions to [generate an SSH key](../ssh/README.md). instructions to [generate an SSH key](../../ssh/README.md).
Then, create a new **Secret Variable** in your project settings on GitLab Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
...@@ -63,7 +63,7 @@ before_script: ...@@ -63,7 +63,7 @@ before_script:
As a final step, add the _public_ key from the one you created earlier to the As a final step, add the _public_ key from the one you created earlier to the
services that you want to have an access to from within the build environment. services that you want to have an access to from within the build environment.
If you are accessing a private GitLab repository you need to add it as a If you are accessing a private GitLab repository you need to add it as a
[deploy key](../ssh/README.md#deploy-keys). [deploy key](../../ssh/README.md#deploy-keys).
That's it! You can now have access to private servers or repositories in your That's it! You can now have access to private servers or repositories in your
build environment. build environment.
...@@ -79,12 +79,12 @@ on, and use that key for all projects that are run on this machine. ...@@ -79,12 +79,12 @@ on, and use that key for all projects that are run on this machine.
First, you need to login to the server that runs your builds. First, you need to login to the server that runs your builds.
Then from the terminal login as the `gitlab-runner` user and generate the SSH Then from the terminal login as the `gitlab-runner` user and generate the SSH
key pair as described in the [SSH keys documentation](../ssh/README.md). key pair as described in the [SSH keys documentation](../../ssh/README.md).
As a final step, add the _public_ key from the one you created earlier to the As a final step, add the _public_ key from the one you created earlier to the
services that you want to have an access to from within the build environment. services that you want to have an access to from within the build environment.
If you are accessing a private GitLab repository you need to add it as a If you are accessing a private GitLab repository you need to add it as a
[deploy key](../ssh/README.md#deploy-keys). [deploy key](../../ssh/README.md#deploy-keys).
Once done, try to login to the remote server in order to accept the fingerprint: Once done, try to login to the remote server in order to accept the fingerprint:
......
# Configuration of your builds with .gitlab-ci.yml # 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) 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 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. 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`) ...@@ -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. or run a script (`test.sh`) in the repository.
Jobs are used to create builds, which are then picked up by Jobs are used to create builds, which are then picked up by
[runners](../runners/README.md) and executed within the environment of the [Runners](../runners/README.md) and executed within the environment of the
runner. What is important, is that each job is run independently from each Runner. What is important, is that each job is run independently from each
other. other.
## .gitlab-ci.yml
The YAML syntax allows for using more complex job specifications than in the The YAML syntax allows for using more complex job specifications than in the
above example: above example:
...@@ -71,7 +111,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: ...@@ -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 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 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 ### before_script
...@@ -86,7 +126,8 @@ The specification of `stages` allows for having flexible multi stage pipelines. ...@@ -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: 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 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: Let's consider the following example, which defines 3 stages:
...@@ -98,9 +139,9 @@ stages: ...@@ -98,9 +139,9 @@ stages:
``` ```
1. First all jobs of `build` are executed in parallel. 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 `build` succeed, 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 `test` succeed, 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 `deploy` succeed, the commit is marked as `success`.
1. If any of the previous jobs fails, the commit is marked as `failed` and no 1. If any of the previous jobs fails, the commit is marked as `failed` and no
jobs of further stage are executed. jobs of further stage are executed.
...@@ -278,14 +319,14 @@ job_name: ...@@ -278,14 +319,14 @@ job_name:
| Keyword | Required | Description | | 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) | | 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) | | 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`) | | stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` | | type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created | | 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 | | 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 | | 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` | | 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| | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
...@@ -294,7 +335,7 @@ job_name: ...@@ -294,7 +335,7 @@ job_name:
### script ### 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 ```yaml
job: job:
...@@ -375,13 +416,13 @@ except master. ...@@ -375,13 +416,13 @@ except master.
### tags ### 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. 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`. 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: assigned to them:
```yaml ```yaml
...@@ -391,7 +432,7 @@ job: ...@@ -391,7 +432,7 @@ job:
- postgres - 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. has both `ruby` AND `postgres` tags defined.
### when ### when
......
...@@ -9,7 +9,7 @@ bundle exec rake setup ...@@ -9,7 +9,7 @@ bundle exec rake setup
``` ```
The `setup` task is a alias for `gitlab: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. Note: `db:setup` calls `db:seed` but this does nothing.
## Run tests ## Run tests
......
...@@ -530,6 +530,16 @@ See the [omniauth integration document](../integration/omniauth.md) ...@@ -530,6 +530,16 @@ See the [omniauth integration document](../integration/omniauth.md)
GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you.
Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
### Adding your Trusted Proxies
If you are using a reverse proxy on an separate machine, you may want to add the
proxy to the trusted proxies list. Otherwise users will appear signed in from the
proxy's IP address.
You can add trusted proxies in `config/gitlab.yml` by customizing the `trusted_proxies`
option in section 1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
for the changes to take effect.
### Custom Redis Connection ### Custom Redis Connection
If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
......
...@@ -120,6 +120,29 @@ OmniAuth provider for an existing user. ...@@ -120,6 +120,29 @@ OmniAuth provider for an existing user.
The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on.
## Configure OmniAuth Providers as External
>**Note:**
This setting was introduced with version 8.7 of GitLab
You can define which OmniAuth providers you want to be `external` so that all users
creating accounts via these providers will not be able to have access to internal
projects. You will need to use the full name of the provider, like `google_oauth2`
for Google. Refer to the examples for the full names of the supported providers.
**For Omnibus installations**
```ruby
gitlab_rails['omniauth_external_providers'] = ['twitter', 'google_oauth2']
```
**For installations from source**
```yaml
omniauth:
external_providers: ['twitter', 'google_oauth2']
```
## Using Custom Omniauth Providers ## Using Custom Omniauth Providers
>**Note:** >**Note:**
......
...@@ -76,3 +76,50 @@ sudo gitlab-ctl reconfigure ...@@ -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. 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
...@@ -37,3 +37,4 @@ Read more on: ...@@ -37,3 +37,4 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](introduction.md) - [Introduction to GitLab Performance Monitoring](introduction.md)
- [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md)
- [InfluxDB Schema](influxdb_schema.md) - [InfluxDB Schema](influxdb_schema.md)
- [Grafana Install/Configuration](grafana_configuration.md
...@@ -61,24 +61,32 @@ contents below and paste it in to the interactive session: ...@@ -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 gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT
CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1 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 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_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 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_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 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_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 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_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 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_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 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_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 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_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 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 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_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 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 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 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_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 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 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_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 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_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 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_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 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_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 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 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 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 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 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 ## Import Dashboards
...@@ -91,21 +99,25 @@ JSON file. ...@@ -91,21 +99,25 @@ JSON file.
Open the dashboard dropdown menu and click 'Import' Open the dashboard dropdown menu and click 'Import'
![Grafana dashboard dropdown](/img/grafana_dashboard_dropdown.png) ![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png)
Click 'Choose file' and browse to the location where you downloaded or cloned Click 'Choose file' and browse to the location where you downloaded or cloned
the dashboard repository. Pick one of the JSON files to import. the dashboard repository. Pick one of the JSON files to import.
![Grafana dashboard import](/img/grafana_dashboard_import.png) ![Grafana dashboard import](img/grafana_dashboard_import.png)
Once the dashboard is imported, be sure to click save icon in the top bar. If Once the dashboard is imported, be sure to click save icon in the top bar. If
you do not save the dashboard after importing it will be removed when you you do not save the dashboard after importing it will be removed when you
navigate away. navigate away.
![Grafana save icon](/img/grafana_save_icon.png) ![Grafana save icon](img/grafana_save_icon.png)
Repeat this process for each dashboard you wish to import. 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 [grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
--- ---
......
...@@ -181,6 +181,7 @@ Read more on: ...@@ -181,6 +181,7 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](introduction.md) - [Introduction to GitLab Performance Monitoring](introduction.md)
- [GitLab Configuration](gitlab_configuration.md) - [GitLab Configuration](gitlab_configuration.md)
- [InfluxDB Schema](influxdb_schema.md) - [InfluxDB Schema](influxdb_schema.md)
- [Grafana Install/Configuration](grafana_configuration.md
[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management [influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ [influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
......
...@@ -85,3 +85,4 @@ Read more on: ...@@ -85,3 +85,4 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](introduction.md) - [Introduction to GitLab Performance Monitoring](introduction.md)
- [GitLab Configuration](gitlab_configuration.md) - [GitLab Configuration](gitlab_configuration.md)
- [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md)
- [Grafana Install/Configuration](grafana_configuration.md
# GitLab JIRA integration # GitLab JIRA integration
_**Note:** >**Note:**
Full JIRA integration was previously exclusive to GitLab Enterprise Edition. Full JIRA integration was previously exclusive to GitLab Enterprise Edition.
With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce]
to GitLab Community Edition as well._ to GitLab Community Edition as well.
--- ---
...@@ -88,8 +88,9 @@ password as they will be needed when configuring GitLab in the next section. ...@@ -88,8 +88,9 @@ password as they will be needed when configuring GitLab in the next section.
### Configuring GitLab ### Configuring GitLab
_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab >**Note:**
7.8 or higher is required._ The currently supported JIRA versions are v6.x and v7.x. and GitLab
7.8 or higher is required.
--- ---
...@@ -113,13 +114,24 @@ Fill in the required details on the page, as described in the table below. ...@@ -113,13 +114,24 @@ Fill in the required details on the page, as described in the table below.
| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | | `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | | `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` | | `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
After saving the configuration, your GitLab project will be able to interact After saving the configuration, your GitLab project will be able to interact
with the linked JIRA project. with the linked JIRA project.
For example, given the settings below:
- the JIRA URL is `https://jira.example.com`
- the project is named `GITLAB`
- the user is named `gitlab`
- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans])
the following screenshot shows how the JIRA service settings should look like.
![JIRA service page](img/jira_service_page.png) ![JIRA service page](img/jira_service_page.png)
[trans]: img/jira_issues_workflow.png
--- ---
## JIRA issues ## JIRA issues
......
...@@ -66,6 +66,35 @@ the target branch. Click **Create directory** to finish. ...@@ -66,6 +66,35 @@ the target branch. Click **Create directory** to finish.
## Create a new branch ## Create a new branch
There are multiple ways to create a branch from GitLab's web interface.
### Create a new branch from an issue
>**Note:**
This feature was [introduced][ce-2808] in GitLab 8.6.
In case your development workflow dictates to have an issue for every merge
request, you can quickly create a branch right on the issue page which will be
tied with the issue itself. You can see a **New Branch** button after the issue
description, unless there is already a branch with the same name or a referenced
merge request.
![New Branch Button](img/new_branch_from_issue.png)
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
`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,
the description field will automatically display the [issue closing pattern]
`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
merge request is merged.
### Create a new branch from a project's dashboard
If you want to make changes to several files before creating a new merge If you want to make changes to several files before creating a new merge
request, you can create a new branch up front. From a project's files page, request, you can create a new branch up front. From a project's files page,
choose **New branch** from the dropdown. choose **New branch** from the dropdown.
...@@ -118,3 +147,6 @@ appear that is labeled **Start a new merge request with these changes**. After ...@@ -118,3 +147,6 @@ appear that is labeled **Start a new merge request with these changes**. After
you commit the changes you will be taken to a new merge request form. you commit the changes you will be taken to a new merge request form.
![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png) ![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
[issue closing pattern]: ../customization/issue_closing.md
...@@ -20,11 +20,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -20,11 +20,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'I should see that I am subscribed' do 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 end
step 'I should see that I am unsubscribed' do 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 end
step 'I click link "Closed"' do step 'I click link "Closed"' do
......
...@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps ...@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
private private
def subscribe_button def subscribe_button
first('.subscribe-button span') first('.label-subscribe-button span')
end end
end end
...@@ -77,11 +77,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -77,11 +77,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end end
step 'I should see that I am subscribed' do 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 end
step 'I should see that I am unsubscribed' do 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 end
step 'I click button "Unsubscribe"' do step 'I click button "Unsubscribe"' do
......
...@@ -227,7 +227,7 @@ module SharedDiffNote ...@@ -227,7 +227,7 @@ module SharedDiffNote
end end
def click_diff_line(code) def click_diff_line(code)
find("button[data-line-code='#{code}']").click find("button[data-line-code='#{code}']").trigger('click')
end end
def click_parallel_diff_line(code, line_type) def click_parallel_diff_line(code, line_type)
......
...@@ -71,13 +71,16 @@ module SharedIssuable ...@@ -71,13 +71,16 @@ module SharedIssuable
step 'I should not see any related merge requests' do step 'I should not see any related merge requests' do
page.within '.issue-details' 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
end end
step 'I should see the "Enterprise fix" related merge request' do 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') expect(page).to have_content('1 Related Merge Request')
end
page.within '#merge-requests ul' do
expect(page).to have_content('Enterprise fix') expect(page).to have_content('Enterprise fix')
end end
end end
......
...@@ -23,8 +23,10 @@ module API ...@@ -23,8 +23,10 @@ module API
# Create group. Available only for users who can create groups. # Create group. Available only for users who can create groups.
# #
# Parameters: # Parameters:
# name (required) - The name of the group # name (required) - The name of the group
# path (required) - The path of the group # path (required) - The path of the group
# description (optional) - The description of the group
# visibility_level (optional) - The visibility level of the group
# Example Request: # Example Request:
# POST /groups # POST /groups
post do post do
...@@ -42,6 +44,28 @@ module API ...@@ -42,6 +44,28 @@ module API
end end
end end
# Update group. Available only for users who can administrate groups.
#
# Parameters:
# id (required) - The ID of a group
# path (optional) - The path of the group
# description (optional) - The description of the group
# visibility_level (optional) - The visibility level of the group
# Example Request:
# PUT /groups/:id
put ':id' do
group = find_group(params[:id])
authorize! :admin_group, group
attrs = attributes_for_keys [:name, :path, :description, :visibility_level]
if ::Groups::UpdateService.new(group, current_user, attrs).execute
present group, with: Entities::GroupDetail
else
render_validation_error!(group)
end
end
# Get a single group, with containing projects # Get a single group, with containing projects
# #
# Parameters: # Parameters:
......
...@@ -91,8 +91,7 @@ module API ...@@ -91,8 +91,7 @@ module API
if can?(current_user, :read_group, group) if can?(current_user, :read_group, group)
group group
else else
forbidden!("#{current_user.username} lacks sufficient "\ not_found!('Group')
"access to #{group.name}")
end end
end end
...@@ -241,6 +240,10 @@ module API ...@@ -241,6 +240,10 @@ module API
render_api_error!('413 Request Entity Too Large', 413) render_api_error!('413 Request Entity Too Large', 413)
end end
def not_modified!
render_api_error!('304 Not Modified', 304)
end
def render_validation_error!(model) def render_validation_error!(model)
if model.errors.any? if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400) render_api_error!(model.errors.messages || '400 Bad Request', 400)
......
...@@ -117,7 +117,7 @@ module API ...@@ -117,7 +117,7 @@ module API
# assignee_id (optional) - The ID of a user to assign issue # assignee_id (optional) - The ID of a user to assign issue
# milestone_id (optional) - The ID of a milestone to assign issue # milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue # labels (optional) - The labels of an issue
# created_at (optional) - The date # created_at (optional) - Date time string, ISO 8601 formatted
# Example Request: # Example Request:
# POST /projects/:id/issues # POST /projects/:id/issues
post ":id/issues" do post ":id/issues" do
...@@ -166,12 +166,15 @@ module API ...@@ -166,12 +166,15 @@ module API
# milestone_id (optional) - The ID of a milestone to assign issue # milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue # labels (optional) - The labels of an issue
# state_event (optional) - The state event of an issue (close|reopen) # state_event (optional) - The state event of an issue (close|reopen)
# updated_at (optional) - Date time string, ISO 8601 formatted
# Example Request: # Example Request:
# PUT /projects/:id/issues/:issue_id # PUT /projects/:id/issues/:issue_id
put ":id/issues/:issue_id" do put ":id/issues/:issue_id" do
issue = user_project.issues.find(params[:issue_id]) issue = user_project.issues.find(params[:issue_id])
authorize! :update_issue, issue 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 # Validate label names in advance
if (errors = validate_label_params(params)).any? if (errors = validate_label_params(params)).any?
...@@ -195,6 +198,29 @@ module API ...@@ -195,6 +198,29 @@ module API
end end
end end
# Move an existing issue
#
# Parameters:
# id (required) - The ID of a project
# issue_id (required) - The ID of a project issue
# to_project_id (required) - The ID of the new project
# Example Request:
# POST /projects/:id/issues/:issue_id/move
post ':id/issues/:issue_id/move' do
required_attributes! [:to_project_id]
issue = user_project.issues.find(params[:issue_id])
new_project = Project.find(params[:to_project_id])
begin
issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
present issue, with: Entities::Issue, current_user: current_user
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
end
end
#
# Delete a project issue # Delete a project issue
# #
# Parameters: # Parameters:
...@@ -208,6 +234,42 @@ module API ...@@ -208,6 +234,42 @@ module API
authorize!(:destroy_issue, issue) authorize!(:destroy_issue, issue)
issue.destroy issue.destroy
end 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 end
end end
...@@ -327,6 +327,42 @@ module API ...@@ -327,6 +327,42 @@ module API
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: Entities::Issue, current_user: current_user present paginate(issues), with: Entities::Issue, current_user: current_user
end 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 end
end end
......
...@@ -61,6 +61,7 @@ module API ...@@ -61,6 +61,7 @@ module API
# id (required) - The ID of a project # id (required) - The ID of a project
# noteable_id (required) - The ID of an issue or snippet # noteable_id (required) - The ID of an issue or snippet
# body (required) - The content of a note # body (required) - The content of a note
# created_at (optional) - The date
# Example Request: # Example Request:
# POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/issues/:noteable_id/notes
# POST /projects/:id/snippets/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes
...@@ -73,6 +74,10 @@ module API ...@@ -73,6 +74,10 @@ module API
noteable_id: params[noteable_id_str] 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 @note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if @note.valid? if @note.valid?
......
...@@ -272,6 +272,40 @@ module API ...@@ -272,6 +272,40 @@ module API
present user_project, with: Entities::Project present user_project, with: Entities::Project
end end
# Star project
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# POST /projects/:id/star
post ':id/star' do
if current_user.starred?(user_project)
not_modified!
else
current_user.toggle_star(user_project)
user_project.reload
present user_project, with: Entities::Project
end
end
# Unstar project
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# DELETE /projects/:id/star
delete ':id/star' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reload
present user_project, with: Entities::Project
else
not_modified!
end
end
# Remove project # Remove project
# #
# Parameters: # Parameters:
......
...@@ -98,7 +98,6 @@ module API ...@@ -98,7 +98,6 @@ module API
authorize! :download_code, user_project authorize! :download_code, user_project
begin begin
RepositoryArchiveCacheWorker.perform_async
header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])
rescue rescue
not_found!('File') not_found!('File')
......
...@@ -14,19 +14,29 @@ class AwardEmoji ...@@ -14,19 +14,29 @@ class AwardEmoji
food_drink: "Food" food_drink: "Food"
}.with_indifferent_access }.with_indifferent_access
CATEGORY_ALIASES = {
symbols: "objects_symbols",
foods: "food_drink",
travel: "travel_places"
}.with_indifferent_access
def self.normilize_emoji_name(name) def self.normilize_emoji_name(name)
aliases[name] || name aliases[name] || name
end end
def self.emoji_by_category def self.emoji_by_category
unless @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| emojis.each do |emoji_name, data|
data["name"] = emoji_name data["name"] = emoji_name
@emoji_by_category[data["category"]] ||= [] # Skip Fitzpatrick(tone) modifiers
@emoji_by_category[data["category"]] << data next if data["category"] == "modifier"
category = CATEGORY_ALIASES[data["category"]] || data["category"]
@emoji_by_category[category] << data
end end
@emoji_by_category = @emoji_by_category.sort.to_h @emoji_by_category = @emoji_by_category.sort.to_h
......
require 'gitlab/git' require 'gitlab/git'
module Gitlab module Gitlab
def self.com?
Gitlab.config.gitlab.url == 'https://gitlab.com'
end
end end
...@@ -34,7 +34,8 @@ module Gitlab ...@@ -34,7 +34,8 @@ module Gitlab
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
two_factor_grace_period: 48, two_factor_grace_period: 48,
akismet_enabled: false akismet_enabled: false,
repository_checks_enabled: true,
) )
end end
......
...@@ -52,11 +52,6 @@ module Gitlab ...@@ -52,11 +52,6 @@ module Gitlab
private private
def redis
# Maybe someday we want to use a connection pool...
@redis ||= Redis.new(url: Gitlab::RedisConfig.url)
end
def redis_key def redis_key
"gitlab:exclusive_lease:#{@key}" "gitlab:exclusive_lease:#{@key}"
end end
......
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
...@@ -104,6 +104,16 @@ module Gitlab ...@@ -104,6 +104,16 @@ module Gitlab
retval retval
end end
# Adds a tag to the current transaction (if any)
#
# name - The name of the tag to add.
# value - The value of the tag.
def self.tag_transaction(name, value)
trans = current_transaction
trans.add_tag(name, value) if trans
end
# When enabled this should be set before being used as the usual pattern # When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe. # "@foo ||= bar" is _not_ thread-safe.
if enabled? if enabled?
......
...@@ -59,8 +59,7 @@ module Gitlab ...@@ -59,8 +59,7 @@ module Gitlab
repository: project.hook_attrs.slice(:name, :url, :description, :homepage) repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
} }
base_data[:object_attributes][:url] = base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note)
Gitlab::UrlBuilder.new(:note).build(note.id)
base_data base_data
end end
......
...@@ -54,6 +54,12 @@ module Gitlab ...@@ -54,6 +54,12 @@ module Gitlab
@user ||= build_new_user @user ||= build_new_user
end end
if external_provider? && @user
@user.external = true
elsif @user
@user.external = false
end
@user @user
end end
...@@ -113,6 +119,10 @@ module Gitlab ...@@ -113,6 +119,10 @@ module Gitlab
end end
end end
def external_provider?
Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
end
def block_after_signup? def block_after_signup?
if creating_linked_ldap_user? if creating_linked_ldap_user?
ldap_config.block_auto_created_users ldap_config.block_auto_created_users
......
module Gitlab module Gitlab
class Redis class Redis
CACHE_NAMESPACE = 'cache:gitlab' CACHE_NAMESPACE = 'cache:gitlab'
SESSION_NAMESPACE = 'session:gitlab'
SIDEKIQ_NAMESPACE = 'resque:gitlab'
attr_reader :url attr_reader :url
......
module Gitlab
class RepositoryCheckLogger < Gitlab::Logger
def self.file_name_noext
'repocheck'
end
end
end
...@@ -4,50 +4,58 @@ module Gitlab ...@@ -4,50 +4,58 @@ module Gitlab
include GitlabRoutingHelper include GitlabRoutingHelper
include ActionView::RecordIdentifier include ActionView::RecordIdentifier
def initialize(type) attr_reader :object
@type = type
end
def build(id) def self.build(object)
case @type new(object).url
when :issue end
build_issue_url(id)
when :merge_request
build_merge_request_url(id)
when :note
build_note_url(id)
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
end end
private private
def build_issue_url(id) def initialize(object)
issue = Issue.find(id) @object = object
issue_url(issue)
end end
def build_merge_request_url(id) def commit_url(opts = {})
merge_request = MergeRequest.find(id) return '' if object.project.nil?
merge_request_url(merge_request)
namespace_project_commit_url({
namespace_id: object.project.namespace,
project_id: object.project,
id: object.id
}.merge!(opts))
end end
def build_note_url(id) def note_url
note = Note.find(id) if object.for_commit?
if note.for_commit? commit_url(id: object.commit_id, anchor: dom_id(object))
namespace_project_commit_url(namespace_id: note.project.namespace,
id: note.commit_id, elsif object.for_issue?
project_id: note.project, issue = Issue.find(object.noteable_id)
anchor: dom_id(note)) issue_url(issue, anchor: dom_id(object))
elsif note.for_issue?
issue = Issue.find(note.noteable_id) elsif object.for_merge_request?
issue_url(issue, anchor: dom_id(note)) merge_request = MergeRequest.find(object.noteable_id)
elsif note.for_merge_request? merge_request_url(merge_request, anchor: dom_id(object))
merge_request = MergeRequest.find(note.noteable_id)
merge_request_url(merge_request, anchor: dom_id(note)) elsif object.for_snippet?
elsif note.for_snippet? snippet = Snippet.find(object.noteable_id)
snippet = Snippet.find(note.noteable_id) project_snippet_url(snippet, anchor: dom_id(object))
project_snippet_url(snippet, anchor: dom_id(note))
end end
end end
end end
......
# 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
...@@ -14,7 +14,7 @@ namespace :gitlab do ...@@ -14,7 +14,7 @@ namespace :gitlab do
puts "" puts ""
end end
Rake::Task["db:setup"].invoke Rake::Task["db:reset"].invoke
Rake::Task["add_limits_mysql"].invoke Rake::Task["add_limits_mysql"].invoke
Rake::Task["setup_postgresql"].invoke Rake::Task["setup_postgresql"].invoke
Rake::Task["db:seed_fu"].invoke Rake::Task["db:seed_fu"].invoke
......
...@@ -157,6 +157,34 @@ describe Projects::MergeRequestsController do ...@@ -157,6 +157,34 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'PUT #update' do
context 'there is no source project' do
let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) }
let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
before do
fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
fork_project.save
merge_request.reload
fork_project.destroy
end
it 'closes MR without errors' do
post :update,
namespace_id: project.namespace.path,
project_id: project.path,
id: merge_request.iid,
merge_request: {
state_event: 'close'
}
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
end
end
end
describe "DELETE #destroy" do describe "DELETE #destroy" do
it "denies access to users unless they're admin or project owner" do it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
......
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
# == 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
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
FactoryGirl.define do FactoryGirl.define do
sequence(:name) { FFaker::Name.name } 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 } email { FFaker::Internet.email }
name name
sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" }
......
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
...@@ -76,6 +76,37 @@ describe 'Filter issues', feature: true do ...@@ -76,6 +76,37 @@ describe 'Filter issues', feature: true do
end end
end end
describe 'Filter issues for label from issues#index', js: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
end
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.within '.labels-filter' do
expect(page).to have_content 'Any Label'
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label')
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content label.title
end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
end
describe 'Filter issues for assignee and label from issues#index' do describe 'Filter issues for assignee and label from issues#index' do
before do before do
......
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
...@@ -10,6 +10,10 @@ describe "Search", feature: true do ...@@ -10,6 +10,10 @@ describe "Search", feature: true do
visit search_path visit search_path
end end
it 'top right search form is not present' do
expect(page).not_to have_selector('.search')
end
describe 'searching for Projects' do describe 'searching for Projects' do
it 'finds a project' do it 'finds a project' do
page.within '.search-holder' do page.within '.search-holder' do
......
#= require notes #= require notes
#= require gl_form
window.gon = {} window.gon = {}
window.disableButtonIfEmptyField = -> null window.disableButtonIfEmptyField = -> null
......
...@@ -16,4 +16,11 @@ describe AwardEmoji do ...@@ -16,4 +16,11 @@ describe AwardEmoji do
end end
end 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 end
...@@ -98,4 +98,29 @@ describe Gitlab::Metrics do ...@@ -98,4 +98,29 @@ describe Gitlab::Metrics do
end end
end end
end end
describe '.tag_transaction' do
context 'without a transaction' do
it 'does nothing' do
expect_any_instance_of(Gitlab::Metrics::Transaction).
not_to receive(:add_tag)
Gitlab::Metrics.tag_transaction(:foo, 'bar')
end
end
context 'with a transaction' do
let(:transaction) { Gitlab::Metrics::Transaction.new }
it 'adds the tag to the transaction' do
expect(Gitlab::Metrics).to receive(:current_transaction).
and_return(transaction)
expect(transaction).to receive(:add_tag).
with(:foo, 'bar')
Gitlab::Metrics.tag_transaction(:foo, 'bar')
end
end
end
end end
...@@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do ...@@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:data) { Gitlab::NoteDataBuilder.build(note, 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 let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
before(:each) do before(:each) do
expect(data).to have_key(:object_attributes) expect(data).to have_key(:object_attributes)
expect(data[:object_attributes]).to have_key(:url) 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[:object_kind]).to eq('note')
expect(data[:user]).to eq(user.hook_attrs) expect(data[:user]).to eq(user.hook_attrs)
end end
......
...@@ -15,20 +15,20 @@ describe Gitlab::OAuth::User, lib: true do ...@@ -15,20 +15,20 @@ describe Gitlab::OAuth::User, lib: true do
end end
let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
describe :persisted? do describe '#persisted?' do
let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
it "finds an existing user based on uid and provider (facebook)" do it "finds an existing user based on uid and provider (facebook)" do
expect( oauth_user.persisted? ).to be_truthy expect( oauth_user.persisted? ).to be_truthy
end end
it "returns false if use is not found in database" do it 'returns false if user is not found in database' do
allow(auth_hash).to receive(:uid).and_return('non-existing') allow(auth_hash).to receive(:uid).and_return('non-existing')
expect( oauth_user.persisted? ).to be_falsey expect( oauth_user.persisted? ).to be_falsey
end end
end end
describe :save do describe '#save' do
def stub_omniauth_config(messages) def stub_omniauth_config(messages)
allow(Gitlab.config.omniauth).to receive_messages(messages) allow(Gitlab.config.omniauth).to receive_messages(messages)
end end
...@@ -40,8 +40,27 @@ describe Gitlab::OAuth::User, lib: true do ...@@ -40,8 +40,27 @@ describe Gitlab::OAuth::User, lib: true do
let(:provider) { 'twitter' } let(:provider) { 'twitter' }
describe 'signup' do describe 'signup' do
shared_examples "to verify compliance with allow_single_sign_on" do shared_examples 'to verify compliance with allow_single_sign_on' do
context "with new allow_single_sign_on enabled syntax" do context 'provider is marked as external' do
it 'should mark user as external' do
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
oauth_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
end
context 'provider was external, now has been removed' do
it 'should mark existing user internal' do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
oauth_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
end
context 'with new allow_single_sign_on enabled syntax' do
before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } before { stub_omniauth_config(allow_single_sign_on: ['twitter']) }
it "creates a user from Omniauth" do it "creates a user from Omniauth" do
...@@ -67,16 +86,16 @@ describe Gitlab::OAuth::User, lib: true do ...@@ -67,16 +86,16 @@ describe Gitlab::OAuth::User, lib: true do
end end
end end
context "with new allow_single_sign_on disabled syntax" do context 'with new allow_single_sign_on disabled syntax' do
before { stub_omniauth_config(allow_single_sign_on: []) } before { stub_omniauth_config(allow_single_sign_on: []) }
it "throws an error" do it 'throws an error' do
expect{ oauth_user.save }.to raise_error StandardError expect{ oauth_user.save }.to raise_error StandardError
end end
end end
context "with old allow_single_sign_on disabled (Default)" do context 'with old allow_single_sign_on disabled (Default)' do
before { stub_omniauth_config(allow_single_sign_on: false) } before { stub_omniauth_config(allow_single_sign_on: false) }
it "throws an error" do it 'throws an error' do
expect{ oauth_user.save }.to raise_error StandardError expect{ oauth_user.save }.to raise_error StandardError
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::UrlBuilder, lib: true do describe Gitlab::UrlBuilder, lib: true do
describe 'When asking for an issue' do describe '.build' do
it 'returns the issue url' do context 'when passing a Commit' do
issue = create(:issue) it 'returns a proper URL' do
url = Gitlab::UrlBuilder.new(:issue).build(issue.id) commit = build_stubbed(:commit)
expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
end
end
describe 'When asking for an merge request' do url = described_class.build(commit)
it 'returns the merge request url' do
merge_request = create(:merge_request) expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) end
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 commit' do context 'when passing an Issue' do
let(:note) { create(:note_on_commit) } it 'returns a proper URL' do
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } issue = build_stubbed(:issue, iid: 42)
it 'returns the note url' do url = described_class.build(issue)
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']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
end
end end
end
describe 'When asking for a note on commit diff' do context 'when passing a MergeRequest' do
let(:note) { create(:note_on_commit_diff) } it 'returns a proper URL' do
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } 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']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" end
end end
end
describe 'When asking for a note on issue' do context 'when passing a Note' do
let(:issue) { create(:issue) } context 'on a Commit' do
let(:note) { create(:note_on_issue, noteable_id: issue.id) } it 'returns a proper URL' do
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } note = build_stubbed(:note_on_commit)
it 'returns the note url' do 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
describe 'When asking for a note on merge request' do expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
let(:merge_request) { create(:merge_request) } end
let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } end
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
it 'returns the note url' do context 'on a CommitDiff' do
expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" it 'returns a proper URL' do
end note = build_stubbed(:note_on_commit_diff)
end
describe 'When asking for a note on merge request diff' do url = described_class.build(note)
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) }
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}#note_#{note.id}" end
end 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 context 'on another object' do
let(:snippet) { create(:project_snippet) } it 'returns a proper URL' do
let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } project = build_stubbed(:project)
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
it 'returns the note url' do expect { described_class.build(project) }.
expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" to raise_error(NotImplementedError, 'No URL builder defined for Project')
end
end
end end
end end
end end
require 'rails_helper'
describe Gitlab, lib: true do
describe '.com?' do
it 'is true when on GitLab.com' do
stub_config_setting(url: 'https://gitlab.com')
expect(described_class.com?).to eq true
end
it 'is false when not on GitLab.com' do
stub_config_setting(url: 'http://example.com')
expect(described_class.com?).to eq false
end
end
end
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
...@@ -195,7 +195,7 @@ describe Issue, models: true do ...@@ -195,7 +195,7 @@ describe Issue, models: true do
before do before do
allow(subject.project.repository).to receive(:branch_names). allow(subject.project.repository).to receive(:branch_names).
and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "branch-#{subject.iid}"]) and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
# Without this stub, the `create(:merge_request)` above fails because it can't find # Without this stub, the `create(:merge_request)` above fails because it can't find
# the source branch. This seems like a reasonable compromise, in comparison with # the source branch. This seems like a reasonable compromise, in comparison with
...@@ -204,17 +204,24 @@ describe Issue, models: true do ...@@ -204,17 +204,24 @@ describe Issue, models: true do
end end
it "selects the right branches when there are no referenced merge requests" do it "selects the right branches when there are no referenced merge requests" do
expect(subject.related_branches(user)).to eq([subject.to_branch_name, "branch-#{subject.iid}"]) expect(subject.related_branches(user)).to eq([subject.to_branch_name, "#{subject.iid}-branch"])
end end
it "selects the right branches when there is a referenced merge request" do it "selects the right branches when there is a referenced merge request" do
merge_request = create(:merge_request, { description: "Closes ##{subject.iid}", merge_request = create(:merge_request, { description: "Closes ##{subject.iid}",
source_project: subject.project, source_project: subject.project,
source_branch: "branch-#{subject.iid}" }) source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user) merge_request.create_cross_references!(user)
expect(subject.referenced_merge_requests).to_not be_empty expect(subject.referenced_merge_requests).to_not be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name]) expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end 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(user)).to eq []
end
end end
it_behaves_like 'an editable mentionable' do it_behaves_like 'an editable mentionable' do
...@@ -231,12 +238,12 @@ describe Issue, models: true do ...@@ -231,12 +238,12 @@ describe Issue, models: true do
describe "#to_branch_name" do describe "#to_branch_name" do
let(:issue) { create(:issue, title: 'testing-issue') } let(:issue) { create(:issue, title: 'testing-issue') }
it "ends with the issue iid" do it 'starts with the issue iid' do
expect(issue.to_branch_name).to match /-#{issue.iid}\z/ expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/
end end
it "contains the issue title if not confidential" do it "contains the issue title if not confidential" do
expect(issue.to_branch_name).to match /\Atesting-issue/ expect(issue.to_branch_name).to match /testing-issue\z/
end end
it "does not contain the issue title if confidential" do it "does not contain the issue title if confidential" do
......
...@@ -21,74 +21,232 @@ ...@@ -21,74 +21,232 @@
require 'spec_helper' require 'spec_helper'
describe BambooService, models: true do describe BambooService, models: true do
describe "Associations" do describe 'Associations' do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
end end
describe "Execute" do describe 'Validations' do
let(:user) { create(:user) } describe '#bamboo_url' do
let(:project) { create(:project) } it 'does not validate the presence of bamboo_url if service is not active' do
bamboo_service = service
context "when a password was previously set" do bamboo_service.active = false
before do
@bamboo_service = BambooService.create( expect(bamboo_service).not_to validate_presence_of(:bamboo_url)
project: create(:project), end
properties: {
bamboo_url: 'http://gitlab.com', it 'validates the presence of bamboo_url if service is active' do
username: 'mic', bamboo_service = service
password: "password" 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 end
it "reset password if url changed" do it 'validates the presence of build_key if service is active' do
@bamboo_service.bamboo_url = 'http://gitlab1.com' bamboo_service = service
@bamboo_service.save bamboo_service.active = true
expect(@bamboo_service.password).to be_nil
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 end
end
it "does not reset password if username changed" do
@bamboo_service.username = "some_name" describe '#password' do
@bamboo_service.save it 'does not validate the presence of password if service is not active' do
expect(@bamboo_service.password).to eq("password") bamboo_service = service
bamboo_service.active = false
expect(bamboo_service).not_to validate_presence_of(:password)
end end
it "does not reset password if new url is set together with password, even if it's the same password" do it 'does not validate the presence of password if username is nil' do
@bamboo_service.bamboo_url = 'http://gitlab_edited.com' bamboo_service = service
@bamboo_service.password = 'password' bamboo_service.active = true
@bamboo_service.save bamboo_service.username = nil
expect(@bamboo_service.password).to eq("password")
expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") expect(bamboo_service).not_to validate_presence_of(:password)
end end
it "should reset password if url changed, even if setter called multiple times" do it 'validates the presence of password if service is active and username is present' do
@bamboo_service.bamboo_url = 'http://gitlab1.com' bamboo_service = service
@bamboo_service.bamboo_url = 'http://gitlab1.com' bamboo_service.active = true
@bamboo_service.save bamboo_service.username = 'john'
expect(@bamboo_service.password).to be_nil
expect(bamboo_service).to validate_presence_of(:password)
end end
end end
end
context "when no password was previously set" do
before do describe 'Callbacks' do
@bamboo_service = BambooService.create( describe 'before_update :reset_password' do
project: create(:project), context 'when a password was previously set' do
properties: { it 'resets password if url changed' do
bamboo_url: 'http://gitlab.com', bamboo_service = service
username: 'mic'
} 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 end
it "saves password if new url is set together with password" do it 'saves password if new url is set together with password when no password was previously set' do
@bamboo_service.bamboo_url = 'http://gitlab_edited.com' bamboo_service = service
@bamboo_service.password = 'password' bamboo_service.password = nil
@bamboo_service.save
expect(@bamboo_service.password).to eq("password") bamboo_service.bamboo_url = 'http://gitlab_edited.com'
expect(@bamboo_service.bamboo_url).to eq("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
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 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
end end
...@@ -3,9 +3,10 @@ require 'spec_helper' ...@@ -3,9 +3,10 @@ require 'spec_helper'
describe BuildsEmailService do describe BuildsEmailService do
let(:build) { create(:ci_build) } let(:build) { create(:ci_build) }
let(:data) { Gitlab::BuildDataBuilder.build(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 it 'sends email' do
service.recipients = 'test@gitlab.com' service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed' data[:build_status] = 'failed'
...@@ -40,4 +41,36 @@ describe BuildsEmailService do ...@@ -40,4 +41,36 @@ describe BuildsEmailService do
service.execute(data) service.execute(data)
end end
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 end
...@@ -21,73 +21,220 @@ ...@@ -21,73 +21,220 @@
require 'spec_helper' require 'spec_helper'
describe TeamcityService, models: true do describe TeamcityService, models: true do
describe "Associations" do describe 'Associations' do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
end end
describe "Execute" do describe 'Validations' do
let(:user) { create(:user) } describe '#teamcity_url' do
let(:project) { create(:project) } it 'does not validate the presence of teamcity_url if service is not active' do
teamcity_service = service
context "when a password was previously set" do teamcity_service.active = false
before do
@teamcity_service = TeamcityService.create( expect(teamcity_service).not_to validate_presence_of(:teamcity_url)
project: create(:project),
properties: {
teamcity_url: 'http://gitlab.com',
username: 'mic',
password: "password"
}
)
end end
it "reset password if url changed" do it 'validates the presence of teamcity_url if service is active' do
@teamcity_service.teamcity_url = 'http://gitlab1.com' teamcity_service = service
@teamcity_service.save teamcity_service.active = true
expect(@teamcity_service.password).to be_nil
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 end
end
it "does not reset password if username changed" do
@teamcity_service.username = "some_name" describe '#username' do
@teamcity_service.save it 'does not validate the presence of username if service is not active' do
expect(@teamcity_service.password).to eq("password") teamcity_service = service
teamcity_service.active = false
expect(teamcity_service).not_to validate_presence_of(:username)
end end
it "does not reset password if new url is set together with password, even if it's the same password" do it 'does not validate the presence of username if username is nil' do
@teamcity_service.teamcity_url = 'http://gitlab_edited.com' teamcity_service = service
@teamcity_service.password = 'password' teamcity_service.active = true
@teamcity_service.save teamcity_service.password = nil
expect(@teamcity_service.password).to eq("password")
expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") expect(teamcity_service).not_to validate_presence_of(:username)
end end
it "should reset password if url changed, even if setter called multiple times" do it 'validates the presence of username if service is active and username is present' do
@teamcity_service.teamcity_url = 'http://gitlab1.com' teamcity_service = service
@teamcity_service.teamcity_url = 'http://gitlab1.com' teamcity_service.active = true
@teamcity_service.save teamcity_service.password = 'secret'
expect(@teamcity_service.password).to be_nil
expect(teamcity_service).to validate_presence_of(:username)
end end
end end
context "when no password was previously set" do describe '#password' do
before do it 'does not validate the presence of password if service is not active' do
@teamcity_service = TeamcityService.create( teamcity_service = service
project: create(:project), teamcity_service.active = false
properties: {
teamcity_url: 'http://gitlab.com', expect(teamcity_service).not_to validate_presence_of(:password)
username: 'mic' 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 end
it "saves password if new url is set together with password" do it 'validates the presence of password if service is active and username is present' do
@teamcity_service.teamcity_url = 'http://gitlab_edited.com' teamcity_service = service
@teamcity_service.password = 'password' teamcity_service.active = true
@teamcity_service.save teamcity_service.username = 'john'
expect(@teamcity_service.password).to eq("password")
expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") expect(teamcity_service).to validate_presence_of(:password)
end end
end 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 end
...@@ -393,6 +393,8 @@ describe Repository, models: true do ...@@ -393,6 +393,8 @@ describe Repository, models: true do
describe '#expire_cache' do describe '#expire_cache' do
it 'expires all caches' do it 'expires all caches' do
expect(repository).to receive(:expire_branch_cache) expect(repository).to receive(:expire_branch_cache)
expect(repository).to receive(:expire_branch_count_cache)
expect(repository).to receive(:expire_tag_count_cache)
repository.expire_cache repository.expire_cache
end end
......
...@@ -42,9 +42,10 @@ describe API::API, api: true do ...@@ -42,9 +42,10 @@ describe API::API, api: true do
end end
end end
it "users not part of the group should get access error" do it 'users not part of the group should get access error' do
get api("/groups/#{group_with_members.id}/members", stranger) get api("/groups/#{group_with_members.id}/members", stranger)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end end
end end
end end
...@@ -165,12 +166,13 @@ describe API::API, api: true do ...@@ -165,12 +166,13 @@ describe API::API, api: true do
end end
end end
describe "DELETE /groups/:id/members/:user_id" do describe 'DELETE /groups/:id/members/:user_id' do
context "when not a member of the group" do context 'when not a member of the group' do
it "should not delete guest's membership of group_with_members" do it "should not delete guest's membership of group_with_members" do
random_user = create(:user) random_user = create(:user)
delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end end
end end
......
...@@ -61,7 +61,8 @@ describe API::API, api: true do ...@@ -61,7 +61,8 @@ describe API::API, api: true do
it "should not return a group not attached to user1" do it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}", user1) get api("/groups/#{group2.id}", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end end
end end
...@@ -92,9 +93,54 @@ describe API::API, api: true do ...@@ -92,9 +93,54 @@ describe API::API, api: true do
it 'should not return a group not attached to user1' do it 'should not return a group not attached to user1' do
get api("/groups/#{group2.path}", user1) get api("/groups/#{group2.path}", user1)
expect(response.status).to eq(404)
end
end
end
describe 'PUT /groups/:id' do
let(:new_group_name) { 'New Group'}
context 'when authenticated as the group owner' do
it 'updates the group' do
put api("/groups/#{group1.id}", user1), name: new_group_name
expect(response.status).to eq(200)
expect(json_response['name']).to eq(new_group_name)
end
it 'returns 404 for a non existing group' do
put api('/groups/1328', user1)
expect(response.status).to eq(404)
end
end
context 'when authenticated as the admin' do
it 'updates the group' do
put api("/groups/#{group1.id}", admin), name: new_group_name
expect(response.status).to eq(200)
expect(json_response['name']).to eq(new_group_name)
end
end
context 'when authenticated as an user that can see the group' do
it 'does not updates the group' do
put api("/groups/#{group1.id}", user2), name: new_group_name
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
context 'when authenticated as an user that cannot see the group' do
it 'returns 404 when trying to update the group' do
put api("/groups/#{group2.id}", user1), name: new_group_name
expect(response.status).to eq(404)
end
end
end end
describe "GET /groups/:id/projects" do describe "GET /groups/:id/projects" do
...@@ -113,7 +159,8 @@ describe API::API, api: true do ...@@ -113,7 +159,8 @@ describe API::API, api: true do
it "should not return a group not attached to user1" do it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1) get api("/groups/#{group2.id}/projects", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end end
end end
...@@ -145,7 +192,8 @@ describe API::API, api: true do ...@@ -145,7 +192,8 @@ describe API::API, api: true do
it 'should not return a group not attached to user1' do it 'should not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1) get api("/groups/#{group2.path}/projects", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end end
end end
end end
...@@ -203,7 +251,8 @@ describe API::API, api: true do ...@@ -203,7 +251,8 @@ describe API::API, api: true do
it "should not remove a group not attached to user1" do it "should not remove a group not attached to user1" do
delete api("/groups/#{group2.id}", user1) delete api("/groups/#{group2.id}", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end end
end end
......
...@@ -3,11 +3,12 @@ require 'spec_helper' ...@@ -3,11 +3,12 @@ require 'spec_helper'
describe API::API, api: true do describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
let(:author) { create(:author) } let(:author) { create(:author) }
let(:assignee) { create(:assignee) } let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) } let(:admin) { create(:user, :admin) }
let!(:project) { create(:project, :public, namespace: user.namespace ) } let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:closed_issue) do let!(:closed_issue) do
create :closed_issue, create :closed_issue,
author: user, author: user,
...@@ -320,13 +321,13 @@ describe API::API, api: true do ...@@ -320,13 +321,13 @@ describe API::API, api: true do
end end
context 'when an admin or owner makes the request' do 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), 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) expect(response.status).to eq(201)
# this take about a second, so probably not equal expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago
end end
end end
end end
...@@ -477,6 +478,18 @@ describe API::API, api: true do ...@@ -477,6 +478,18 @@ describe API::API, api: true do
expect(json_response['labels']).to include 'label2' expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed" expect(json_response['state']).to eq "closed"
end 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 end
describe "DELETE /projects/:id/issues/:issue_id" do describe "DELETE /projects/:id/issues/:issue_id" do
...@@ -501,4 +514,114 @@ describe API::API, api: true do ...@@ -501,4 +514,114 @@ describe API::API, api: true do
end end
end end
end end
describe '/projects/:id/issues/:issue_id/move' do
let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project.id
expect(response.status).to eq(201)
expect(json_response['project_id']).to eq(target_project.id)
end
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: project.id
expect(response.status).to eq(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project2.id
expect(response.status).to eq(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
it 'moves the issue to another namespace if I am admin' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
to_project_id: target_project2.id
expect(response.status).to eq(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/123/move", user),
to_project_id: target_project.id
expect(response.status).to eq(404)
end
end
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/123/issues/#{issue.id}/move", user),
to_project_id: target_project.id
expect(response.status).to eq(404)
end
end
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: 123
expect(response.status).to eq(404)
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 end
...@@ -516,6 +516,48 @@ describe API::API, api: true do ...@@ -516,6 +516,48 @@ describe API::API, api: true do
end end
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 def mr_with_later_created_and_updated_at_time
merge_request merge_request
merge_request.created_at += 1.hour merge_request.created_at += 1.hour
......
...@@ -158,6 +158,19 @@ describe API::API, api: true do ...@@ -158,6 +158,19 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
expect(response.status).to eq(401) expect(response.status).to eq(401)
end 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 end
context "when noteable is a Snippet" do context "when noteable is a Snippet" do
......
...@@ -1020,6 +1020,54 @@ describe API::API, api: true do ...@@ -1020,6 +1020,54 @@ describe API::API, api: true do
end end
end end
describe 'POST /projects/:id/star' do
context 'on an unstarred project' do
it 'stars the project' do
expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
expect(response.status).to eq(201)
expect(json_response['star_count']).to eq(1)
end
end
context 'on a starred project' do
before do
user.toggle_star(project)
project.reload
end
it 'does not modify the star count' do
expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
expect(response.status).to eq(304)
end
end
end
describe 'DELETE /projects/:id/star' do
context 'on a starred project' do
before do
user.toggle_star(project)
project.reload
end
it 'unstars the project' do
expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
expect(response.status).to eq(200)
expect(json_response['star_count']).to eq(0)
end
end
context 'on an unstarred project' do
it 'does not modify the star count' do
expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
expect(response.status).to eq(304)
end
end
end
describe 'DELETE /projects/:id' do describe 'DELETE /projects/:id' do
context 'when authenticated as user' do context 'when authenticated as user' do
it 'should remove project' do it 'should remove project' do
......
...@@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do ...@@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do
def transfer_project(project, user, new_namespace) def transfer_project(project, user, new_namespace)
Projects::TransferService.new(project, user).execute(new_namespace) Projects::TransferService.new(project, user).execute(new_namespace)
end 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 end
...@@ -4,6 +4,9 @@ describe PostReceive do ...@@ -4,6 +4,9 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } 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(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } 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 context "as a resque worker" do
it "reponds to #perform" do it "reponds to #perform" do
...@@ -11,11 +14,43 @@ describe PostReceive do ...@@ -11,11 +14,43 @@ describe PostReceive do
end end
end end
context "webhook" do describe "#process_project_changes" do
let(:project) { create(:project) } before do
let(:key) { create(:key, user: project.owner) } allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
let(:key_id) { key.shell_id } 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 it "fetches the correct project" do
expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
PostReceive.new.perform(pwd(project), key_id, base64_changes) PostReceive.new.perform(pwd(project), key_id, base64_changes)
......
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
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment