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.
v 8.7.0 (unreleased)
- The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse)
- Fix revoking of authorized OAuth applications (Connor Shea)
- All service classes (those residing in app/services) are now instrumented (Yorick Peterse)
- Developers can now add custom tags to transactions (Yorick Peterse)
- Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse)
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Add setting for customizing the list of trusted proxies !3524
- Allow projects to be transfered to a lower visibility level group
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389 (Yorick Peterse)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Expose label description in API (Mariusz Jachimowicz)
- Allow back dating on issues when created through the API
- API: Ability to update a group (Robert Schilling)
- API: Ability to move issues (Robert Schilling)
- Fix Error 500 after renaming a project path (Stan Hu)
- Fix a bug whith trailing slash in teamcity_url (Charles May)
- Allow back dating on issues when created or updated through the API
- Allow back dating on issue notes when created through the API
- Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530
- Allow Omniauth providers to be marked as `external` !3657
- Add endpoints to archive or unarchive a project !3372
- Fix a bug whith trailing slash in bamboo_url
- Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
- Add automated repository integrity checks
- API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling)
- API: Ability to star and unstar a project (Robert Schilling)
- Add default scope to projects to exclude projects pending deletion
- Allow to close merge requests which source projects(forks) are deleted.
- Ensure empty recipients are rejected in BuildsEmailService
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups
- Fix high CPU usage when PostReceive receives refs/merge-requests/<id>
- Hide `Create a group` help block when creating a new project in a group
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
......@@ -39,11 +57,18 @@ v 8.7.0 (unreleased)
- Fix admin/projects when using visibility levels on search (PotHix)
- Build status notifications
- API: Expose user location (Robert Schilling)
- API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600
- Sanitize branch names created for confidential issues
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
- API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
- Diffs load at the correct point when linking from from number
- Selected diff rows highlight
- Fix emoji catgories in the emoji picker
v 8.6.6
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
......
......@@ -22,7 +22,17 @@
#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap
#= require bootstrap/affix
#= require bootstrap/alert
#= require bootstrap/button
#= require bootstrap/collapse
#= require bootstrap/dropdown
#= require bootstrap/modal
#= require bootstrap/scrollspy
#= require bootstrap/tab
#= require bootstrap/transition
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2
#= require raphael
#= require g.raphael
......
......@@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
e.preventDefault()
$form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable()
$submit_button = $form.find('input[type=submit], button[type=submit]')
return if $submit_button.attr('disabled')
$submit_button.disable()
$form.submit()
# If the user tabs to a submit button on a `js-quick-submit` form, display a
......
......@@ -28,26 +28,26 @@ class Dispatcher
new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
new GLForm($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
when 'projects:compare:show'
new Diff()
when 'projects:issues:new','projects:issues:edit'
shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.issue-form'))
new GLForm($('.issue-form'))
new IssuableForm($('.issue-form'))
when 'projects:merge_requests:new', 'projects:merge_requests:edit'
new Diff()
shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.merge-request-form'))
new GLForm($('.merge-request-form'))
new IssuableForm($('.merge-request-form'))
when 'projects:tags:new'
new ZenMode()
new DropzoneInput($('.tag-form'))
new GLForm($('.tag-form'))
when 'projects:releases:edit'
new ZenMode()
new DropzoneInput($('.release-form'))
new GLForm($('.release-form'))
when 'projects:merge_requests:show'
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
......@@ -137,7 +137,7 @@ class Dispatcher
new Wikis()
shortcut_handler = new ShortcutsNavigation()
new ZenMode()
new DropzoneInput($('.wiki-form'))
new GLForm($('.wiki-form'))
when 'snippets'
shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show'
......
......@@ -15,11 +15,13 @@ class @DropzoneInput
project_uploads_path = window.project_uploads_path or null
max_file_size = gon.max_file_size or 10
form_textarea = $(form).find("textarea.markdown-area")
form_textarea = $(form).find(".js-gfm-input")
form_textarea.wrap "<div class=\"div-dropzone\"></div>"
form_textarea.on 'paste', (event) =>
handlePaste(event)
$mdArea = $(form_textarea).closest('.md-area')
$(form).setupMarkdownPreview()
form_dropzone = $(form).find('.div-dropzone')
......@@ -49,17 +51,16 @@ class @DropzoneInput
$(".div-dropzone-alert").alert "close"
dragover: ->
form_textarea.addClass "div-dropzone-focus"
$mdArea.addClass 'is-dropzone-hover'
form.find(".div-dropzone-hover").css "opacity", 0.7
return
dragleave: ->
form_textarea.removeClass "div-dropzone-focus"
$mdArea.removeClass 'is-dropzone-hover'
form.find(".div-dropzone-hover").css "opacity", 0
return
drop: ->
form_textarea.removeClass "div-dropzone-focus"
form.find(".div-dropzone-hover").css "opacity", 0
form_textarea.focus()
return
......
class @GLForm
constructor: (@form) ->
@textarea = @form.find('textarea.js-gfm-input')
# Before we start, we should clean up any previous data for this form
@destroy()
# Setup the form
@setupForm()
@form.data 'gl-form', @
destroy: ->
# Clean form listeners
@clearEventListeners()
@form.data 'gl-form', null
setupForm: ->
isNewForm = @form.is(':not(.gfm-form)')
@form.removeClass 'js-new-note-form'
if isNewForm
@form.find('.div-dropzone').remove()
@form.addClass('gfm-form')
disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button')
# remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup()
new DropzoneInput(@form)
autosize(@textarea)
# form and textarea event listeners
@addEventListeners()
# hide discard button
@form.find('.js-note-discard').hide()
@form.show()
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
addEventListeners: ->
@textarea.on 'focus', ->
$(@).closest('.md-area').addClass 'is-focused'
@textarea.on 'blur', ->
$(@).closest('.md-area').removeClass 'is-focused'
......@@ -10,6 +10,9 @@ class @Issue
@initTaskList()
@initIssueBtnEventListeners()
@initMergeRequests()
@initRelatedBranches()
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
......@@ -69,3 +72,23 @@ class @Issue
type: 'PATCH'
url: $('form.js-issuable-update').attr('action')
data: patchData
initMergeRequests: ->
$container = $('#merge-requests')
$.getJSON($container.data('url'))
.error ->
new Flash('Failed to load referenced merge requests', 'alert')
.success (data) ->
if 'html' of data
$container.html(data.html)
initRelatedBranches: ->
$container = $('#related-branches')
$.getJSON($container.data('url'))
.error ->
new Flash('Failed to load related branches', 'alert')
.success (data) ->
if 'html' of data
$container.html(data.html)
......@@ -34,7 +34,7 @@ class @LabelsSelect
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
<span class="label color-label" style="background-color: <%= label.color %>;">
<span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;">
<%= label.title %>
</span>
</a>
......@@ -165,6 +165,8 @@ class @LabelsSelect
.html(template)
$sidebarCollapsedValue.text(labelCount)
$('.has-tooltip', $value).tooltip(container: 'body')
$value
.find('a')
.each((i) ->
......@@ -218,7 +220,7 @@ class @LabelsSelect
selectable: true
toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label'
if selected and selected.title?
selected.title
else
defaultLabel
......
......@@ -85,8 +85,10 @@ class @MergeRequestTabs
scrollToElement: (container) ->
if window.location.hash
$el = $("div#{container} #{window.location.hash}")
$('body').scrollTo($el.offset().top) if $el.length
navBarHeight = $('.navbar-gitlab').outerHeight()
$el = $("#{container} #{window.location.hash}")
$.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length
# Activate a tab based on the current action
activateTab: (action) ->
......@@ -152,12 +154,38 @@ class @MergeRequestTabs
@_get
url: "#{source}.json" + @_location.search
success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html
$('#diffs').html data.html
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
$('div#diffs .js-syntax-highlight').syntaxHighlight()
$('#diffs .js-syntax-highlight').syntaxHighlight()
@expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
@scrollToElement("#diffs")
@highlighSelectedLine()
$(document)
.off 'click', '.diff-line-num a'
.on 'click', '.diff-line-num a', (e) =>
e.preventDefault()
window.location.hash = $(e.currentTarget).attr 'href'
@highlighSelectedLine()
@scrollToElement("#diffs")
highlighSelectedLine: ->
$('.hll').removeClass 'hll'
locationHash = window.location.hash
if locationHash isnt ''
hashClassString = ".#{locationHash.replace('#', '')}"
$diffLine = $(locationHash)
if $diffLine.is ':not(tr)'
$diffLine = $("td#{locationHash}, td#{hashClassString}")
else
$diffLine = $('td', $diffLine)
$diffLine.addClass 'hll'
diffLineTop = $diffLine.offset().top
navBarHeight = $('.navbar-gitlab').outerHeight()
loadBuilds: (source) ->
return if @buildsLoaded
......
......@@ -283,32 +283,10 @@ class @Notes
show the form
###
setupNoteForm: (form) ->
disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button")
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
# hide discard button
form.find('.js-note-discard').hide()
# setup preview buttons
previewButton = form.find(".js-md-preview-button")
new GLForm form
textarea = form.find(".js-note-text")
textarea.on "input", ->
if $(this).val().trim() isnt ""
previewButton.removeClass("turn-off").addClass "turn-on"
else
previewButton.removeClass("turn-on").addClass "turn-off"
textarea.on 'focus', ->
$(this).closest('.md-area').addClass 'is-focused'
textarea.on 'blur', ->
$(this).closest('.md-area').removeClass 'is-focused'
autosize(textarea)
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
......@@ -317,11 +295,6 @@ class @Notes
form.find("#note_noteable_id").val()
]
# remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
###
Called in response to the new note form being submitted
......@@ -375,34 +348,15 @@ class @Notes
note = $(this).closest(".note")
note.addClass "is-editting"
form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)')
if isNewForm
form.addClass('gfm-form')
form.addClass('current-note-edit-form')
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
if isNewForm
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
textarea = form.find("textarea")
textarea.focus()
if isNewForm
autosize(textarea)
new GLForm form
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
# modify it, so let's clear it and re-set it!
value = textarea.val()
textarea.val ""
textarea.val value
if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
form.find(".js-note-text").focus()
###
Called in response to clicking the edit note link
......@@ -559,6 +513,9 @@ class @Notes
removeDiscussionNoteForm: (form)->
row = form.closest("tr")
glForm = form.data 'gl-form'
glForm.destroy()
form.find(".js-note-text").data("autosave").reset()
# show the reply button (will only work for replies)
......@@ -570,7 +527,6 @@ class @Notes
# only remove the form
form.remove()
cancelDiscussionForm: (e) =>
e.preventDefault()
form = $(e.target).closest(".js-discussion-note-form")
......
......@@ -2,7 +2,7 @@ class @Subscription
constructor: (container) ->
$container = $(container)
@url = $container.attr('data-url')
@subscribe_button = $container.find('.subscribe-button')
@subscribe_button = $container.find('.js-subscribe-button')
@subscription_status = $container.find('.subscription-status')
@subscribe_button.unbind('click').click(@toggleSubscription)
......
/**
* Styles that apply to all GFM related forms.
*/
.issue-form, .merge-request-form, .wiki-form {
.description {
height: 16em;
border-top-left-radius: 0;
}
}
.wiki-form {
.description {
height: 26em;
}
}
.milestone-form {
.description {
height: 14em;
}
}
.gfm-commit, .gfm-commit_range {
font-family: $monospace_font;
......
.div-dropzone-wrapper {
.div-dropzone {
position: relative;
margin-bottom: -5px;
.div-dropzone-focus {
border-color: #66afe9 !important;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important;
outline: 0 !important;
}
.div-dropzone-hover {
position: absolute;
top: 50%;
left: 50%;
margin-top: -0.5em;
margin-left: -0.6em;
margin-top: -11.5px;
margin-left: -15px;
opacity: 0;
font-size: 50px;
font-size: 30px;
transition: opacity 200ms ease-in-out;
pointer-events: none;
}
......
......@@ -43,7 +43,6 @@
@import "bootstrap/modals";
@import "bootstrap/tooltip";
@import "bootstrap/popovers";
@import "bootstrap/carousel";
// Utility classes
.clearfix {
......
......@@ -250,14 +250,6 @@ a > code {
* Textareas intended for GFM
*
*/
.js-gfm-input {
font-family: $monospace_font;
color: $gl-text-color;
}
.md-preview {
}
.strikethrough {
text-decoration: line-through;
}
......
......@@ -150,6 +150,7 @@ $light-grey-header: #faf9f9;
*/
$gl-primary: $blue-normal;
$gl-success: $green-normal;
$gl-success-focus: rgba($gl-success, .4);
$gl-info: $blue-normal;
$gl-warning: $orange-normal;
$gl-danger: $red-normal;
......
......@@ -21,6 +21,12 @@
// Diff line
.line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #557;
border-color: darken(#557, 15%);
}
.diff-line-num.new, .line_content.new {
@include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
}
......
......@@ -21,6 +21,12 @@
// Diff line
.line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #49483e;
border-color: darken(#49483e, 15%);
}
.diff-line-num.new, .line_content.new {
@include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
}
......
......@@ -21,6 +21,12 @@
// Diff line
.line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #174652;
border-color: darken(#174652, 15%);
}
.diff-line-num.new, .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
}
......
......@@ -21,6 +21,12 @@
// Diff line
.line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #ddd8c5;
border-color: darken(#ddd8c5, 15%);
}
.diff-line-num.new, .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
}
......
......@@ -21,6 +21,12 @@
// Diff line
.line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #f8eec7;
border-color: darken(#f8eec7, 15%);
}
.diff-line-num {
&.old {
background-color: $line-number-old;
......
......@@ -67,6 +67,24 @@
line-height: $code_line_height;
font-size: $code_font_size;
&.noteable_line {
position: relative;
&.old {
&:before {
content: '-';
position: absolute;
}
}
&.new {
&:before {
content: '+';
position: absolute;
}
}
}
span {
white-space: pre;
}
......@@ -391,3 +409,23 @@
margin-bottom: 0;
}
}
.file-holder {
.diff-line-num:not(.js-unfold-bottom) {
a {
&:before {
content: attr(data-linenumber);
}
}
}
}
.discussion {
.diff-content {
.diff-line-num {
&:before {
content: attr(data-linenumber);
}
}
}
}
......@@ -173,12 +173,6 @@
}
}
.subscribe-button {
span {
margin-top: 0;
}
}
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
display: none;
......@@ -322,3 +316,9 @@
color: #8c8c8c;
}
}
.issuable-form-padding-top {
@media (min-width: $screen-sm-min) {
padding-top: 7px;
}
}
......@@ -79,19 +79,30 @@
color: $white-light;
}
@mixin labels-mobile {
@media (max-width: $screen-xs-min) {
display: block;
width: 100%;
margin-left: 0;
padding: 10px 0;
}
}
.manage-labels-list {
.prepend-left-10 {
.prepend-left-10, .prepend-description-left {
display: inline-block;
width: 40%;
vertical-align: middle;
@media (max-width: $screen-xs-min) {
display: block;
width: 100%;
margin-left: 0;
padding: 10px 0;
@include labels-mobile;
}
.prepend-description-left {
width: 57%;
@include labels-mobile;
}
.pull-info-right {
......@@ -106,7 +117,7 @@
padding: 6px;
color: $gl-text-color;
&.subscribe-button {
&.label-subscribe-button {
padding-left: 0;
}
}
......
......@@ -40,6 +40,7 @@
}
.note-textarea {
display: block;
padding: 10px 0;
font-family: $regular_font;
border: 0;
......@@ -63,7 +64,7 @@
&.is-focused {
border-color: $focus-border-color;
box-shadow: 0 0 2px rgba(#000, .2),
box-shadow: 0 0 2px $black-transparent,
0 0 4px rgba($focus-border-color, .4);
.comment-toolbar,
......@@ -72,6 +73,17 @@
}
}
&.is-dropzone-hover {
border-color: $gl-success;
box-shadow: 0 0 2px $black-transparent,
0 0 4px $gl-success-focus;
.comment-toolbar,
.nav-links {
border-color: $gl-success;
}
}
p {
code {
white-space: normal;
......
......@@ -276,8 +276,7 @@ ul.notes {
.diff-file tr.line_holder {
@mixin show-add-diff-note {
filter: alpha(opacity=100);
opacity: 1.0;
display: inline-block;
}
.add-diff-note {
......@@ -291,13 +290,8 @@ ul.notes {
position: absolute;
z-index: 10;
width: 32px;
transition: all 0.2s ease;
// "hide" it by default
opacity: 0.0;
filter: alpha(opacity=0);
display: none;
&:hover {
background: $gl-info;
color: #fff;
......
......@@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to admin_runners_path
end
def clear_repository_check_states
RepositoryCheck::ClearWorker.perform_async
redirect_to(
admin_application_settings_path,
notice: 'Started asynchronous removal of all repository check states.'
)
end
private
def set_application_setting
......@@ -82,6 +91,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_enabled,
:akismet_api_key,
:email_author_in_body,
:repository_checks_enabled,
restricted_visibility_levels: [],
import_sources: []
)
......
class Admin::ProjectsController < Admin::ApplicationController
before_action :project, only: [:show, :transfer]
before_action :project, only: [:show, :transfer, :repository_check]
before_action :group, only: [:show, :transfer]
def index
......@@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present?
@projects = @projects.non_archived unless params[:with_archived].present?
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
......@@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController
redirect_to admin_namespace_project_path(@project.namespace, @project)
end
def repository_check
RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id)
redirect_to(
admin_namespace_project_path(@project.namespace, @project),
notice: 'Repository check was triggered.'
)
end
protected
def project
......
......@@ -3,6 +3,7 @@ require 'fogbugz'
class ApplicationController < ActionController::Base
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
......@@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
before_action :sentry_user_context
before_action :sentry_context
before_action :default_headers
before_action :add_gon_variables
before_action :configure_permitted_parameters, if: :devise_controller?
......@@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base
protected
def sentry_user_context
if Rails.env.production? && current_application_settings.sentry_enabled && current_user
def sentry_context
if Rails.env.production? && current_application_settings.sentry_enabled
if current_user
Raven.user_context(
id: current_user.id,
email: current_user.email,
username: current_user.username,
)
end
Raven.tags_context(program: sentry_program_context)
end
......@@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base
end
end
def add_gon_variables
gon.api_version = API::API.version
gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.default_issues_tracker = Project.new.default_issue_tracker.to_param
gon.max_file_size = current_application_settings.max_attachment_size
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
if current_user
gon.current_user_id = current_user.id
gon.api_token = current_user.private_token
end
end
def validate_user_service_ticket!
return unless signed_in? && session[:service_tickets]
......
......@@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController
@last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
......
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
before_action :add_gon_variables
layout 'profile'
......
......@@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show]
before_action :issue,
only: [:edit, :update, :show, :referenced_merge_requests, :related_branches]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show]
......@@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow issues bulk update
before_action :authorize_admin_issues!, only: [:bulk_update]
# Cross-reference merge requests
before_action :closed_by_merge_requests, only: [:show]
respond_to :html
def index
......@@ -65,8 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
@merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches(current_user)
respond_to do |format|
format.html
......@@ -118,15 +114,38 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def referenced_merge_requests
@merge_requests = @issue.referenced_merge_requests(current_user)
@closed_by_merge_requests = @issue.closed_by_merge_requests(current_user)
respond_to do |format|
format.json do
render json: {
html: view_to_html_string('projects/issues/_merge_requests')
}
end
end
end
def related_branches
merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches(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
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
protected
def issue
......
......@@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
RepositoryArchiveCacheWorker.perform_async
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
head :ok
rescue => ex
......
......@@ -40,10 +40,11 @@ module DiffHelper
(unfold) ? 'unfold js-unfold' : ''
end
def diff_line_content(line)
def diff_line_content(line, line_type = nil)
if line.blank?
" &nbsp;".html_safe
else
line[0] = ' ' if %w[new old].include?(line_type)
line
end
end
......
class RepositoryCheckMailer < BaseMailer
def notify(failed_count)
if failed_count == 1
@message = "One project failed its last repository check"
else
@message = "#{failed_count} projects failed their last repository check"
end
mail(
to: User.admins.pluck(:email),
subject: @message
)
end
end
......@@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false
akismet_enabled: false,
repository_checks_enabled: true,
)
end
......
......@@ -150,13 +150,11 @@ class Commit
end
def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace
data = {
id: id,
message: safe_message,
timestamp: committed_date.xmlschema,
url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}",
url: Gitlab::UrlBuilder.build(self),
author: {
name: author_name,
email: author_email
......
......@@ -105,11 +105,10 @@ class Issue < ActiveRecord::Base
end
# All branches containing the current issue's ID, except for
# those with a merge request open (that the current user can see)
# referencing the current issue.
# those with a merge request open referencing the current issue.
def related_branches(current_user)
branches_with_iid = project.repository.branch_names.select do |branch|
branch.end_with?("-#{iid}")
branch =~ /\A#{iid}-(?!\d+-stable)/i
end
branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
......@@ -161,7 +160,7 @@ class Issue < ActiveRecord::Base
if self.confidential?
"issue-#{iid}"
else
"#{title.parameterize}-#{iid}"
"#{iid}-#{title.parameterize}"
end
end
......
......@@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches
validate :validate_branches, unless: :allow_broken
validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
......@@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base
end
if opened? || reopened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
errors.add :validate_branches,
......@@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base
def hook_attrs
attrs = {
source: source_project.hook_attrs,
source: source_project.try(:hook_attrs),
target: target_project.hook_attrs,
last_commit: nil,
work_in_progress: work_in_progress?
......
# == 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
end
def build_info(sha)
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
if username.blank? && password.blank?
@response = HTTParty.get(parsed_url.to_s, verify: false)
@response = HTTParty.get(url, verify: false)
else
get_url = "#{url}&os_authType=basic"
url << '&os_authType=basic'
auth = {
username: username,
password: password,
password: password
}
@response = HTTParty.get(get_url, verify: false, basic_auth: auth)
@response = HTTParty.get(url, verify: false, basic_auth: auth)
end
end
......@@ -101,11 +101,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
"#{bamboo_url}/browse/#{build_key}"
URI.join(bamboo_url, "/browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key']
"#{bamboo_url}/browse/#{result_key}"
URI.join(bamboo_url, "/browse/#{result_key}").to_s
end
end
......@@ -134,7 +134,7 @@ class BambooService < CiService
return unless supported_events.include?(data[:object_kind])
# Bamboo requires a GET and does not take any data.
self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}",
verify: false)
url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
self.class.get(url, verify: false)
end
end
......@@ -23,7 +23,7 @@ class BuildsEmailService < Service
prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_builds
validates :recipients, presence: true, if: :activated?
validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
def initialize_properties
if properties.nil?
......@@ -87,10 +87,14 @@ class BuildsEmailService < Service
end
def all_recipients(data)
all_recipients = recipients.split(',').compact.reject(&:blank?)
all_recipients = []
unless recipients.blank?
all_recipients += recipients.split(',').compact.reject(&:blank?)
end
if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}"
all_recipients << data[:user][:email]
end
all_recipients
......
......@@ -85,13 +85,15 @@ class TeamcityService < CiService
end
def build_info(sha)
url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\
"branch:unspecified:any,number:#{sha}")
url = URI.join(
teamcity_url,
"/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
).to_s
auth = {
username: username,
password: password,
password: password
}
@response = HTTParty.get("#{url}", verify: false, basic_auth: auth)
@response = HTTParty.get(url, verify: false, basic_auth: auth)
end
def build_page(sha, ref)
......@@ -100,12 +102,14 @@ class TeamcityService < CiService
if @response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
"#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}"
URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
else
# If actual build link is available, go to build result page.
built_id = @response['build']['id']
"#{teamcity_url}/viewLog.html?buildId=#{built_id}"\
"&buildTypeId=#{build_type}"
URI.join(
teamcity_url,
"/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
).to_s
end
end
......@@ -140,7 +144,8 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
self.class.post(
URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
'</build>',
......
......@@ -253,6 +253,8 @@ class Repository
# This ensures this particular cache is flushed after the first commit to a
# new repository.
expire_emptiness_caches if empty?
expire_branch_count_cache
expire_tag_count_cache
end
def expire_branch_cache(branch_name = nil)
......
......@@ -3,7 +3,7 @@ module Issues
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
issue_url = Gitlab::UrlBuilder.build(issue)
issue_data[:object_attributes].merge!(url: issue_url, action: action)
issue_data
end
......
......@@ -20,8 +20,7 @@ module MergeRequests
def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
hook_data[:object_attributes][:url] = merge_request_url
hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
hook_data[:object_attributes][:action] = action
hook_data
end
......
......@@ -51,7 +51,7 @@ module MergeRequests
# be interpreted as the use wants to close that issue on this project
# Pattern example: 112-fix-mep-mep
# Will lead to appending `Closes #112` to the description
if match = merge_request.source_branch.match(/-(\d+)\z/)
if match = merge_request.source_branch.match(/\A(\d+)-/)
iid = match[1]
closes_issue = "Closes ##{iid}"
......
......@@ -26,22 +26,28 @@ module Projects
GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
ensure
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
@project.update_column(:pushes_since_gc, 0)
end
end
def needed?
@project.pushes_since_gc >= 10
end
def increment!
Gitlab::Metrics.measure(:increment_pushes_since_gc) do
@project.increment!(:pushes_since_gc)
end
end
private
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end
end
end
......@@ -34,8 +34,9 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists")
end
# Apply new namespace id
# Apply new namespace id and visibility level
project.namespace = new_namespace
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
# Notifications
......
......@@ -222,7 +222,7 @@ class SystemNoteService
# Called when a branch is created from the 'new branch' button on a issue
# Example note text:
#
# "Started branch `issue-branch-button-201`"
# "Started branch `201-issue-branch-button`"
def self.new_issue_branch(issue, project, author, branch)
h = Gitlab::Routing.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
......
......@@ -271,5 +271,24 @@
.col-sm-10
= f.text_field :sentry_dsn, class: 'form-control'
%fieldset
%legend Repository Checks
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :repository_checks_enabled do
= f.check_box :repository_checks_enabled
Enable Repository Checks
.help-block
GitLab will periodically run
%a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group
.col-sm-offset-2.col-sm-10
= link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger]
%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
......
......@@ -3,7 +3,7 @@
.row.prepend-top-default
%aside.col-md-3
.admin-filter
.panel.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
.form-group
= label_tag :name, 'Name:'
......@@ -38,7 +38,13 @@
%span.descr
= visibility_level_icon(level)
= label
%hr
%fieldset
%strong Problems
.checkbox
= label_tag :last_repository_check_failed do
= check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed]
%span Last repository check failed
= hidden_field_tag :sort, params[:sort]
= button_tag "Search", class: "btn submit btn-primary"
= link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
......
......@@ -5,6 +5,16 @@
%i.fa.fa-pencil-square-o
Edit
%hr
- if @project.last_repository_check_failed?
.row
.col-md-12
.panel
.panel-heading.alert.alert-danger
Last repository check
= "(#{time_ago_in_words(@project.last_repository_check_at)} ago)"
failed. See
= link_to 'repocheck.log', admin_logs_path
for error messages.
.row
.col-md-6
.panel.panel-default
......@@ -95,6 +105,32 @@
.col-sm-offset-2.col-sm-10
= f.submit 'Transfer', class: 'btn btn-primary'
.panel.panel-default.repository-check
.panel-heading
Repository check
.panel-body
= form_for @project, url: repository_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f|
.form-group
- if @project.last_repository_check_at.nil?
This repository has never been checked.
- else
This repository was last checked
= @project.last_repository_check_at.to_s(:medium) + '.'
The check
- if @project.last_repository_check_failed?
= succeed '.' do
%strong.cred failed
See
= link_to 'repocheck.log', admin_logs_path
for error messages.
- else
passed.
= link_to icon('question-circle'), help_page_path('administration', 'repository_checks')
.form-group
= f.submit 'Trigger repository check', class: 'btn btn-primary'
.col-md-6
- if @group
.panel.panel-default
......
......@@ -68,7 +68,7 @@
%td= app.name
%td= token.created_at
%td= token.scopes
%td= render 'delete_form', application: app
%td= render 'doorkeeper/authorized_applications/delete_form', application: app
- @authorized_anonymous_tokens.each do |token|
%tr
%td
......
......@@ -8,7 +8,7 @@
This will create milestone in every selected project
%hr
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
- if @milestone.errors.any?
#error_explanation
......@@ -27,7 +27,7 @@
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
.form-group
......
......@@ -8,7 +8,7 @@
.navbar-collapse.collapse
%ul.nav.navbar-nav
%li.hidden-sm.hidden-xs
= render 'layouts/search'
= render 'layouts/search' unless current_controller?(:search)
%li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
......
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..."
= f.text_area attr, class: classes, placeholder: placeholder
- else
= text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..."
= text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
- if @lines.present?
- if @form.unfold? && @form.since != 1 && !@form.bottom?
%tr.line_holder{ id: @form.since }
= render "projects/diffs/match_line", {line: @match_line,
line_old: @form.since, line_new: @form.since, bottom: false, new_file: false}
= render "projects/diffs/match_line", { line: @match_line,
line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
- @lines.each_with_index do |line, index|
- line_new = index + @form.since
- line_old = line_new - @form.offset
%tr.line_holder
%td.old_line.diff-line-num{data: {linenumber: line_old}}
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_old), "#"
%td.new_line.diff-line-num
%td.new_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_new) , "#"
%td.line_content.noteable_line==#{' ' * @form.indent}#{line}
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
%tr.line_holder{ id: @form.to }
= render "projects/diffs/match_line", {line: @match_line,
line_old: @form.to, line_new: @form.to, bottom: true, new_file: false}
= render "projects/diffs/match_line", { line: @match_line,
line_old: @form.to, line_new: @form.to, bottom: true, new_file: false }
- type = line.type
%tr.line_holder{id: line_code, class: type}
%tr.line_holder{ id: line_code, class: type }
- case type
- when 'match'
= render "projects/diffs/match_line", {line: line.text,
line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
= render "projects/diffs/match_line", { line: line.text,
line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file }
- when 'nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num{class: type}
- link_text = raw(type == "new" ? "&nbsp;" : line.old_pos)
%td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "new" ? "&nbsp;".html_safe : line.old_pos
- if defined?(plain) && plain
= link_text
- else
= link_to link_text, "##{line_code}", id: line_code
= link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(line_code)
%td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
- link_text = raw(type == "old" ? "&nbsp;" : line.new_pos)
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? "&nbsp;".html_safe : line.new_pos
- if defined?(plain) && plain
= link_text
- else
= link_to link_text, "##{line_code}", id: line_code
%td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
= link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
%td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type)
......@@ -14,11 +14,11 @@
%td.new_line.diff-line-num
%td.line_content.parallel.match= left[:text]
- else
%td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"}
%td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"}
= link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(left[:line_code], 'old')
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
- if right[:type] == 'new'
- new_line_class = 'new'
......@@ -27,11 +27,11 @@
- new_line_class = nil
- new_line_code = left[:line_code]
%td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }}
%td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }}
= link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(right[:line_code], 'new')
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
- if @reply_allowed
- comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
......
......@@ -210,6 +210,7 @@
%li Be careful. Changing the project's namespace can have unintended side effects.
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location.
%li Project visibility level will be changed to match namespace rules when transfering to a group.
.form-actions
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- else
......
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
:javascript
......
......@@ -64,9 +64,11 @@
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
.merge-requests
= render 'merge_requests'
= render 'related_branches'
#merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)}
// This element is filled in using JavaScript.
#related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)}
// This element is filled in using JavaScript.
.content-block.content-block-small
= render 'new_branch'
......
......@@ -14,7 +14,7 @@
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
.subscription-status{data: {status: label_subscription_status(label)}}
%a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}}
%button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } }
%span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project
......
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input js-quick-submit' } do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript
......
......@@ -10,7 +10,7 @@
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
......
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
= form_errors(@milestone)
.row
.col-md-6
.form-group
......@@ -11,7 +10,7 @@
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/notes/hints'
.clearfix
.error-alert
......
......@@ -2,7 +2,7 @@
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field'
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.note-form-actions.clearfix
......
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
......@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text'
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.error-alert
......
......@@ -20,11 +20,9 @@
%td.new_line.diff-line-num= "..."
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num
= raw(type == "new" ? "&nbsp;" : line.old_pos)
%td.new_line.diff-line-num
= raw(type == "old" ? "&nbsp;" : line.new_pos)
%td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text)
%td.old_line.diff-line-num{ data: { linenumber: type == "new" ? "&nbsp;".html_safe : line.old_pos } }
%td.new_line.diff-line-num{ data: { linenumber: type == "old" ? "&nbsp;".html_safe : line.new_pos } }
%td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type)
- if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
......@@ -9,9 +9,9 @@
%strong #{@tag.name}
.prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f|
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
.error-alert
.form-actions.prepend-top-default
......
......@@ -10,7 +10,7 @@
New Tag
%hr
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do
.form-group
= label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
......@@ -30,7 +30,7 @@
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'description form-control'
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
......
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
= f.hidden_field :title, value: @page.title
......@@ -11,7 +11,7 @@
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
= render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
= render 'projects/notes/hints'
.clearfix
......
%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
- note_url = Gitlab::UrlBuilder.new(:note).build(note.id)
- note_url = Gitlab::UrlBuilder.build(note)
- noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row
%h5.note-search-caption.str-truncated
......
......@@ -29,7 +29,8 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
classes: 'description form-control'
classes: 'note-textarea',
placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.clearfix
.error-alert
......@@ -70,13 +71,13 @@
- if can? current_user, :admin_milestone, issuable.project
= link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank
.form-group
- has_labels = issuable.project.labels.any?
= f.label :label_ids, "Labels", class: 'control-label'
.col-sm-10
- if issuable.project.labels.any?
.col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
- if has_labels
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
{ selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else
.prepend-top-10
%span.light No labels yet.
&nbsp;
- if can? current_user, :admin_label, issuable.project
......@@ -128,8 +129,6 @@
- else
.pull-right
- if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
= link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-grouped' do
= icon('trash-o')
Delete
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
- if params[:label_name].present?
= hidden_field_tag(:label_name, params[:label_name])
.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
= h(params[:label_name].presence || "Label")
= icon('chevron-down')
......
......@@ -128,7 +128,7 @@
.title.hide-collapsed
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
......
......@@ -4,15 +4,16 @@
%li
%span.label-row
%span.label-name
= link_to milestones_label_path(options) do
- render_colored_label(label, tooltip: false)
%span.prepend-left-10
%span.prepend-description-left
= markdown(label.description, pipeline: :single_line)
.pull-right
%strong.issues-count
.pull-info-right
%span.append-right-20
= link_to milestones_label_path(options.merge(state: 'opened')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
%strong.issues-count
%span.append-right-20
= link_to milestones_label_path(options.merge(state: 'closed')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
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
if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
else
elsif Gitlab::Git.branch_ref?(ref)
GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
end
......
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
# end
puts "\n== Preparing database =="
system "bin/rake db:setup"
system "bin/rake db:reset"
puts "\n== Removing old logs and tempfiles =="
system "rm -f log/*"
......
......@@ -46,6 +46,15 @@ production: &base
#
# 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')
# user: git
......@@ -155,7 +164,17 @@ production: &base
# Flag stuck CI builds as failed
stuck_ci_builds_worker:
cron: "0 0 * * *"
# Periodically run 'git fsck' on all repositories. If started more than
# once per hour you will have concurrent 'git fsck' jobs.
repository_check_worker:
cron: "20 * * * *"
# Send admin emails once a day
admin_email_worker:
cron: "0 0 * * *"
# Remove outdated repository archives
repository_archive_cache_worker:
cron: "0 * * * *"
#
# 2. GitLab CI settings
......@@ -304,6 +323,13 @@ production: &base
# (default: 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
# 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:
......
......@@ -129,6 +129,7 @@ Settings['omniauth'] ||= Settingslogic.new({})
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['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['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?
......@@ -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['restricted_signup_domains'] ||= []
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({})
Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *'
Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker'
Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
#
# GitLab Shell
......
......@@ -14,7 +14,7 @@ if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
redis_config = Gitlab::Redis.redis_store_options
redis_config[:namespace] = 'session:gitlab'
redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
......
SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
Sidekiq.configure_server do |config|
config.redis = {
url: Gitlab::Redis.url,
namespace: SIDEKIQ_REDIS_NAMESPACE
namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
}
config.server_middleware do |chain|
......@@ -30,6 +28,6 @@ end
Sidekiq.configure_client do |config|
config.redis = {
url: Gitlab::Redis.url,
namespace: SIDEKIQ_REDIS_NAMESPACE
namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
}
end
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
member do
put :transfer
post :repository_check
end
resources :runner_projects
......@@ -281,6 +282,7 @@ Rails.application.routes.draw do
resource :application_settings, only: [:show, :update] do
resources :services
put :reset_runners_token
put :clear_repository_check_states
end
resources :labels
......@@ -701,6 +703,8 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
get :referenced_merge_requests
get :related_branches
end
collection do
post :bulk_update
......
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 @@
#
class MigrateNewNotificationSetting < ActiveRecord::Migration
def up
timestamp = Time.now
timestamp = Time.now.strftime('%F %T')
execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL"
end
......
class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration
def change
add_column :application_settings, :repository_checks_enabled, :boolean, default: true
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160331223143) do
ActiveRecord::Schema.define(version: 20160412140240) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -77,6 +77,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do
t.string "akismet_api_key"
t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility"
t.boolean "repository_checks_enabled", default: true
end
create_table "audit_events", force: :cascade do |t|
......@@ -743,6 +744,8 @@ ActiveRecord::Schema.define(version: 20160331223143) do
t.boolean "public_builds", default: true, null: false
t.string "main_language"
t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
......@@ -752,6 +755,7 @@ ActiveRecord::Schema.define(version: 20160331223143) do
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
......
......@@ -3,7 +3,7 @@
## User documentation
- [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 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).
......@@ -31,6 +31,7 @@
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
......
# 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.
| ------------- | ----------- |
| `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. |
| `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. |
| `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. |
......
......@@ -126,6 +126,87 @@ Parameters:
- `id` (required) - The ID or path of a group
- `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
Removes group with all projects inside.
......
......@@ -298,6 +298,7 @@ PUT /projects/:id/issues/:issue_id
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
```bash
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
......@@ -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
```
## 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 are done via the [notes](notes.md) resource.
......@@ -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:
- `id` (required) - The ID of a project
- `issue_id` (required) - The ID of an issue
- `body` (required) - The content of a note
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
### Modify existing issue note
......
......@@ -491,6 +491,132 @@ Parameters:
- `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
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
multiple projects. This makes it easier to maintain and update runners.
**Specific runners** are useful for jobs that have special requirements or for
projects with a very demand. If a job has certain requirements, you can set
projects with a specific demand. If a job has certain requirements, you can set
up the specific runner with this in mind, while not having to do this for all
runners. For example, if you want to deploy a certain project, you can setup
a specific runner to have the right credentials for this.
......
......@@ -30,7 +30,7 @@ This is the universal solution which works with any type of executor
## SSH keys when using the Docker executor
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
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
......@@ -63,7 +63,7 @@ before_script:
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.
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
build environment.
......@@ -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.
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
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
[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:
......
# Configuration of your builds with .gitlab-ci.yml
This document describes the usage of `.gitlab-ci.yml`, the file that is used by
GitLab Runner to manage your project's builds.
If you want a quick introduction to GitLab CI, follow our
[quick start guide](../quick_start/README.md).
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [.gitlab-ci.yml](#gitlab-ci-yml)
- [image and services](#image-and-services)
- [before_script](#before_script)
- [stages](#stages)
- [types](#types)
- [variables](#variables)
- [cache](#cache)
- [cache:key](#cache-key)
- [Jobs](#jobs)
- [script](#script)
- [stage](#stage)
- [only and except](#only-and-except)
- [tags](#tags)
- [when](#when)
- [artifacts](#artifacts)
- [artifacts:name](#artifacts-name)
- [dependencies](#dependencies)
- [Hidden jobs](#hidden-jobs)
- [Special YAML features](#special-yaml-features)
- [Anchors](#anchors)
- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
- [Skipping builds](#skipping-builds)
- [Examples](#examples)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
---
## .gitlab-ci.yml
From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML)
file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root
of your repository and contains definitions of how your project should be built.
......@@ -23,12 +65,10 @@ Of course a command can execute code directly (`./configure;make;make install`)
or run a script (`test.sh`) in the repository.
Jobs are used to create builds, which are then picked up by
[runners](../runners/README.md) and executed within the environment of the
runner. What is important, is that each job is run independently from each
[Runners](../runners/README.md) and executed within the environment of the
Runner. What is important, is that each job is run independently from each
other.
## .gitlab-ci.yml
The YAML syntax allows for using more complex job specifications than in the
above example:
......@@ -71,7 +111,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
This allows to specify a custom Docker image and a list of services that can be
used for time of the build. The configuration of this feature is covered in
separate document: [Use Docker](../docker/README.md).
[a separate document](../docker/README.md).
### before_script
......@@ -86,7 +126,8 @@ The specification of `stages` allows for having flexible multi stage pipelines.
The ordering of elements in `stages` defines the ordering of builds' execution:
1. Builds of the same stage are run in parallel.
1. Builds of next stage are run after success.
1. Builds of the next stage are run after the jobs from the previous stage
complete successfully.
Let's consider the following example, which defines 3 stages:
......@@ -98,9 +139,9 @@ stages:
```
1. First all jobs of `build` are executed in parallel.
1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel.
1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel.
1. If all jobs of `deploy` succeeds, the commit is marked as `success`.
1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
1. If all jobs of `deploy` succeed, the commit is marked as `success`.
1. If any of the previous jobs fails, the commit is marked as `failed` and no
jobs of further stage are executed.
......@@ -278,14 +319,14 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
| script | yes | Defines a shell script which is executed by Runner |
| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select runner |
| tags | no | Defines a list of tags which are used to select Runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
......@@ -294,7 +335,7 @@ job_name:
### script
`script` is a shell script which is executed by the runner. For example:
`script` is a shell script which is executed by the Runner. For example:
```yaml
job:
......@@ -375,13 +416,13 @@ except master.
### tags
`tags` is used to select specific runners from the list of all runners that are
`tags` is used to select specific Runners from the list of all Runners that are
allowed to run this project.
During the registration of a runner, you can specify the runner's tags, for
During the registration of a Runner, you can specify the Runner's tags, for
example `ruby`, `postgres`, `development`.
`tags` allow you to run builds with runners that have the specified tags
`tags` allow you to run builds with Runners that have the specified tags
assigned to them:
```yaml
......@@ -391,7 +432,7 @@ job:
- postgres
```
The specification above, will make sure that `job` is built by a runner that
The specification above, will make sure that `job` is built by a Runner that
has both `ruby` AND `postgres` tags defined.
### when
......
......@@ -9,7 +9,7 @@ bundle exec rake setup
```
The `setup` task is a alias for `gitlab:setup`.
This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing.
## Run tests
......
......@@ -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.
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
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.
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
>**Note:**
......
......@@ -76,3 +76,50 @@ sudo gitlab-ctl reconfigure
```
On the sign in page there should now be a "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in.
## Apache 2.4 / GitLab 8.6 update
The order of the first 2 Location directives is important. If they are reversed,
you will not get a shibboleth session!
```
<Location />
Require all granted
ProxyPassReverse http://127.0.0.1:8181
ProxyPassReverse http://YOUR_SERVER_FQDN/
</Location>
<Location /users/auth/shibboleth/callback>
AuthType shibboleth
ShibRequestSetting requireSession 1
ShibUseHeaders On
Require shib-session
</Location>
Alias /shibboleth-sp /usr/share/shibboleth
<Location /shibboleth-sp>
Require all granted
</Location>
<Location /Shibboleth.sso>
SetHandler shib
</Location>
RewriteEngine on
#Don't escape encoded characters in api requests
RewriteCond %{REQUEST_URI} ^/api/v3/.*
RewriteCond %{REQUEST_URI} !/Shibboleth.sso
RewriteCond %{REQUEST_URI} !/shibboleth-sp
RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE]
#Forward all requests to gitlab-workhorse except existing files
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_URI} ^/uploads/.*
RewriteCond %{REQUEST_URI} !/Shibboleth.sso
RewriteCond %{REQUEST_URI} !/shibboleth-sp
RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA]
RequestHeader set X_FORWARDED_PROTO 'https'
RequestHeader set X-Forwarded-Ssl on
```
\ No newline at end of file
......@@ -37,3 +37,4 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](introduction.md)
- [InfluxDB Configuration](influxdb_configuration.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:
```
CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT
CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1
CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END
CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END
CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM rails_transactions GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM sidekiq_transactions GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM rails_method_calls GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM sidekiq_method_calls GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM rails_method_calls GROUP BY time(1m), method END;
CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM sidekiq_method_calls GROUP BY time(1m), method END;
CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM rails_memory_usage GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM sidekiq_memory_usage GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM sidekiq_file_descriptors GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM rails_file_descriptors GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM rails_gc_statistics GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM rails_gc_statistics GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM rails_gc_statistics GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_internal_allowed_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_allowed_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_internal_allowed_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_internal_allowed_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_authorized_keys_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_internal_authorized_keys_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.rails_transaction_timings FROM rails_transactions GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.sidekiq_transaction_timings FROM sidekiq_transactions GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_transaction_counts FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END;
CREATE CONTINUOUS QUERY grape_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.grape_transaction_timings FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END;
```
## Import Dashboards
......@@ -91,21 +99,25 @@ JSON file.
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
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
you do not save the dashboard after importing it will be removed when you
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.
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
---
......
......@@ -181,6 +181,7 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](introduction.md)
- [GitLab Configuration](gitlab_configuration.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 documentation]: https://docs.influxdata.com/influxdb/v0.9/
......
......@@ -85,3 +85,4 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](introduction.md)
- [GitLab Configuration](gitlab_configuration.md)
- [InfluxDB Configuration](influxdb_configuration.md)
- [Grafana Install/Configuration](grafana_configuration.md
# GitLab JIRA integration
_**Note:**
>**Note:**
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]
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.
### Configuring GitLab
_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab
7.8 or higher is required._
>**Note:**
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.
| `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). |
| `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
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)
[trans]: img/jira_issues_workflow.png
---
## JIRA issues
......
......@@ -66,6 +66,35 @@ the target branch. Click **Create directory** to finish.
## 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
request, you can create a new branch up front. From a project's files page,
choose **New branch** from the dropdown.
......@@ -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.
![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
end
step 'I should see that I am subscribed' do
expect(find('.subscribe-button span')).to have_content 'Unsubscribe'
expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
expect(find('.subscribe-button span')).to have_content 'Subscribe'
expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click link "Closed"' do
......
......@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
private
def subscribe_button
first('.subscribe-button span')
first('.label-subscribe-button span')
end
end
......@@ -77,11 +77,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see that I am subscribed' do
expect(find('.subscribe-button span')).to have_content 'Unsubscribe'
expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
expect(find('.subscribe-button span')).to have_content 'Subscribe'
expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click button "Unsubscribe"' do
......
......@@ -227,7 +227,7 @@ module SharedDiffNote
end
def click_diff_line(code)
find("button[data-line-code='#{code}']").click
find("button[data-line-code='#{code}']").trigger('click')
end
def click_parallel_diff_line(code, line_type)
......
......@@ -71,13 +71,16 @@ module SharedIssuable
step 'I should not see any related merge requests' do
page.within '.issue-details' do
expect(page).not_to have_content('.merge-requests')
expect(page).not_to have_content('#merge-requests .merge-requests-title')
end
end
step 'I should see the "Enterprise fix" related merge request' do
page.within '.merge-requests' do
page.within '#merge-requests .merge-requests-title' do
expect(page).to have_content('1 Related Merge Request')
end
page.within '#merge-requests ul' do
expect(page).to have_content('Enterprise fix')
end
end
......
......@@ -25,6 +25,8 @@ module API
# Parameters:
# name (required) - The name 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:
# POST /groups
post do
......@@ -42,6 +44,28 @@ module API
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
#
# Parameters:
......
......@@ -91,8 +91,7 @@ module API
if can?(current_user, :read_group, group)
group
else
forbidden!("#{current_user.username} lacks sufficient "\
"access to #{group.name}")
not_found!('Group')
end
end
......@@ -241,6 +240,10 @@ module API
render_api_error!('413 Request Entity Too Large', 413)
end
def not_modified!
render_api_error!('304 Not Modified', 304)
end
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
......
......@@ -117,7 +117,7 @@ module API
# assignee_id (optional) - The ID of a user to assign issue
# milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue
# created_at (optional) - The date
# created_at (optional) - Date time string, ISO 8601 formatted
# Example Request:
# POST /projects/:id/issues
post ":id/issues" do
......@@ -166,12 +166,15 @@ module API
# milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue
# state_event (optional) - The state event of an issue (close|reopen)
# updated_at (optional) - Date time string, ISO 8601 formatted
# Example Request:
# PUT /projects/:id/issues/:issue_id
put ":id/issues/:issue_id" do
issue = user_project.issues.find(params[:issue_id])
authorize! :update_issue, issue
attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event]
keys = [:title, :description, :assignee_id, :milestone_id, :state_event]
keys << :updated_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys)
# Validate label names in advance
if (errors = validate_label_params(params)).any?
......@@ -195,6 +198,29 @@ module API
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
#
# Parameters:
......@@ -208,6 +234,42 @@ module API
authorize!(:destroy_issue, issue)
issue.destroy
end
# Subscribes to a project issue
#
# Parameters:
# id (required) - The ID of a project
# issue_id (required) - The ID of a project issue
# Example Request:
# POST /projects/:id/issues/:issue_id/subscription
post ':id/issues/:issue_id/subscription' do
issue = user_project.issues.find(params[:issue_id])
if issue.subscribed?(current_user)
not_modified!
else
issue.toggle_subscription(current_user)
present issue, with: Entities::Issue, current_user: current_user
end
end
# Unsubscribes from a project issue
#
# Parameters:
# id (required) - The ID of a project
# issue_id (required) - The ID of a project issue
# Example Request:
# DELETE /projects/:id/issues/:issue_id/subscription
delete ':id/issues/:issue_id/subscription' do
issue = user_project.issues.find(params[:issue_id])
if issue.subscribed?(current_user)
issue.unsubscribe(current_user)
present issue, with: Entities::Issue, current_user: current_user
else
not_modified!
end
end
end
end
end
......@@ -327,6 +327,42 @@ module API
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: Entities::Issue, current_user: current_user
end
# Subscribes to a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - The ID of a merge request
# Example Request:
# POST /projects/:id/issues/:merge_request_id/subscription
post "#{path}/subscription" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
if merge_request.subscribed?(current_user)
not_modified!
else
merge_request.toggle_subscription(current_user)
present merge_request, with: Entities::MergeRequest, current_user: current_user
end
end
# Unsubscribes from a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - The ID of a merge request
# Example Request:
# DELETE /projects/:id/merge_requests/:merge_request_id/subscription
delete "#{path}/subscription" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
if merge_request.subscribed?(current_user)
merge_request.unsubscribe(current_user)
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
not_modified!
end
end
end
end
end
......
......@@ -61,6 +61,7 @@ module API
# id (required) - The ID of a project
# noteable_id (required) - The ID of an issue or snippet
# body (required) - The content of a note
# created_at (optional) - The date
# Example Request:
# POST /projects/:id/issues/:noteable_id/notes
# POST /projects/:id/snippets/:noteable_id/notes
......@@ -73,6 +74,10 @@ module API
noteable_id: params[noteable_id_str]
}
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
@note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if @note.valid?
......
......@@ -272,6 +272,40 @@ module API
present user_project, with: Entities::Project
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
#
# Parameters:
......
......@@ -98,7 +98,6 @@ module API
authorize! :download_code, user_project
begin
RepositoryArchiveCacheWorker.perform_async
header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])
rescue
not_found!('File')
......
......@@ -14,19 +14,29 @@ class AwardEmoji
food_drink: "Food"
}.with_indifferent_access
CATEGORY_ALIASES = {
symbols: "objects_symbols",
foods: "food_drink",
travel: "travel_places"
}.with_indifferent_access
def self.normilize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = {}
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
@emoji_by_category[data["category"]] ||= []
@emoji_by_category[data["category"]] << data
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = CATEGORY_ALIASES[data["category"]] || data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
......
require 'gitlab/git'
module Gitlab
def self.com?
Gitlab.config.gitlab.url == 'https://gitlab.com'
end
end
......@@ -34,7 +34,8 @@ module Gitlab
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
akismet_enabled: false
akismet_enabled: false,
repository_checks_enabled: true,
)
end
......
......@@ -52,11 +52,6 @@ module Gitlab
private
def redis
# Maybe someday we want to use a connection pool...
@redis ||= Redis.new(url: Gitlab::RedisConfig.url)
end
def redis_key
"gitlab:exclusive_lease:#{@key}"
end
......
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
retval
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
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
......
......@@ -59,8 +59,7 @@ module Gitlab
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
base_data[:object_attributes][:url] =
Gitlab::UrlBuilder.new(:note).build(note.id)
base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note)
base_data
end
......
......@@ -54,6 +54,12 @@ module Gitlab
@user ||= build_new_user
end
if external_provider? && @user
@user.external = true
elsif @user
@user.external = false
end
@user
end
......@@ -113,6 +119,10 @@ module Gitlab
end
end
def external_provider?
Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
end
def block_after_signup?
if creating_linked_ldap_user?
ldap_config.block_auto_created_users
......
module Gitlab
class Redis
CACHE_NAMESPACE = 'cache:gitlab'
SESSION_NAMESPACE = 'session:gitlab'
SIDEKIQ_NAMESPACE = 'resque:gitlab'
attr_reader :url
......
module Gitlab
class RepositoryCheckLogger < Gitlab::Logger
def self.file_name_noext
'repocheck'
end
end
end
......@@ -4,50 +4,58 @@ module Gitlab
include GitlabRoutingHelper
include ActionView::RecordIdentifier
def initialize(type)
@type = type
end
attr_reader :object
def build(id)
case @type
when :issue
build_issue_url(id)
when :merge_request
build_merge_request_url(id)
when :note
build_note_url(id)
def self.build(object)
new(object).url
end
def url
case object
when Commit
commit_url
when Issue
issue_url(object)
when MergeRequest
merge_request_url(object)
when Note
note_url
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
end
private
def build_issue_url(id)
issue = Issue.find(id)
issue_url(issue)
end
def build_merge_request_url(id)
merge_request = MergeRequest.find(id)
merge_request_url(merge_request)
end
def build_note_url(id)
note = Note.find(id)
if note.for_commit?
namespace_project_commit_url(namespace_id: note.project.namespace,
id: note.commit_id,
project_id: note.project,
anchor: dom_id(note))
elsif note.for_issue?
issue = Issue.find(note.noteable_id)
issue_url(issue, anchor: dom_id(note))
elsif note.for_merge_request?
merge_request = MergeRequest.find(note.noteable_id)
merge_request_url(merge_request, anchor: dom_id(note))
elsif note.for_snippet?
snippet = Snippet.find(note.noteable_id)
project_snippet_url(snippet, anchor: dom_id(note))
def initialize(object)
@object = object
end
def commit_url(opts = {})
return '' if object.project.nil?
namespace_project_commit_url({
namespace_id: object.project.namespace,
project_id: object.project,
id: object.id
}.merge!(opts))
end
def note_url
if object.for_commit?
commit_url(id: object.commit_id, anchor: dom_id(object))
elsif object.for_issue?
issue = Issue.find(object.noteable_id)
issue_url(issue, anchor: dom_id(object))
elsif object.for_merge_request?
merge_request = MergeRequest.find(object.noteable_id)
merge_request_url(merge_request, anchor: dom_id(object))
elsif object.for_snippet?
snippet = Snippet.find(object.noteable_id)
project_snippet_url(snippet, anchor: dom_id(object))
end
end
end
......
# 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
puts ""
end
Rake::Task["db:setup"].invoke
Rake::Task["db:reset"].invoke
Rake::Task["add_limits_mysql"].invoke
Rake::Task["setup_postgresql"].invoke
Rake::Task["db:seed_fu"].invoke
......
......@@ -157,6 +157,34 @@ describe Projects::MergeRequestsController do
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
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
......
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
sequence(:name) { FFaker::Name.name }
factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do
factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do
email { FFaker::Internet.email }
name
sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" }
......
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
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
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
visit search_path
end
it 'top right search form is not present' do
expect(page).not_to have_selector('.search')
end
describe 'searching for Projects' do
it 'finds a project' do
page.within '.search-holder' do
......
#= require notes
#= require gl_form
window.gon = {}
window.disableButtonIfEmptyField = -> null
......
......@@ -16,4 +16,11 @@ describe AwardEmoji do
end
end
end
describe '.emoji_by_category' do
it "only contains known categories" do
undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys
expect(undefined_categories).to be_empty
end
end
end
......@@ -98,4 +98,29 @@ describe Gitlab::Metrics do
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
......@@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
before(:each) do
expect(data).to have_key(:object_attributes)
expect(data[:object_attributes]).to have_key(:url)
expect(data[:object_attributes][:url]).to eq(note_url)
expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note))
expect(data[:object_kind]).to eq('note')
expect(data[:user]).to eq(user.hook_attrs)
end
......
......@@ -15,20 +15,20 @@ describe Gitlab::OAuth::User, lib: true do
end
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') }
it "finds an existing user based on uid and provider (facebook)" do
expect( oauth_user.persisted? ).to be_truthy
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')
expect( oauth_user.persisted? ).to be_falsey
end
end
describe :save do
describe '#save' do
def stub_omniauth_config(messages)
allow(Gitlab.config.omniauth).to receive_messages(messages)
end
......@@ -40,8 +40,27 @@ describe Gitlab::OAuth::User, lib: true do
let(:provider) { 'twitter' }
describe 'signup' do
shared_examples "to verify compliance with allow_single_sign_on" do
context "with new allow_single_sign_on enabled syntax" do
shared_examples 'to verify compliance with allow_single_sign_on' 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']) }
it "creates a user from Omniauth" do
......@@ -67,16 +86,16 @@ describe Gitlab::OAuth::User, lib: true do
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: []) }
it "throws an error" do
it 'throws an error' do
expect{ oauth_user.save }.to raise_error StandardError
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) }
it "throws an error" do
it 'throws an error' do
expect{ oauth_user.save }.to raise_error StandardError
end
end
......
require 'spec_helper'
describe Gitlab::UrlBuilder, lib: true do
describe 'When asking for an issue' do
it 'returns the issue url' do
issue = create(:issue)
url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
describe '.build' do
context 'when passing a Commit' do
it 'returns a proper URL' do
commit = build_stubbed(:commit)
url = described_class.build(commit)
expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
end
end
context 'when passing an Issue' do
it 'returns a proper URL' do
issue = build_stubbed(:issue, iid: 42)
url = described_class.build(issue)
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
it 'returns the merge request url' do
merge_request = create(:merge_request)
url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
context 'when passing a MergeRequest' do
it 'returns a proper URL' do
merge_request = build_stubbed(:merge_request, iid: 42)
url = described_class.build(merge_request)
expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
end
end
describe 'When asking for a note on commit' do
let(:note) { create(:note_on_commit) }
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
context 'when passing a Note' do
context 'on a Commit' do
it 'returns a proper URL' do
note = build_stubbed(:note_on_commit)
url = described_class.build(note)
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}"
end
end
describe 'When asking for a note on commit diff' do
let(:note) { create(:note_on_commit_diff) }
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
context 'on a CommitDiff' do
it 'returns a proper URL' do
note = build_stubbed(:note_on_commit_diff)
url = described_class.build(note)
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}"
end
end
describe 'When asking for a note on issue' do
let(:issue) { create(:issue) }
let(:note) { create(:note_on_issue, noteable_id: issue.id) }
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
context '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)
it 'returns the note url' do
expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
end
end
describe 'When asking for a note on merge request' do
let(:merge_request) { create(:merge_request) }
let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) }
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
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)
it 'returns the note url' do
expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
end
end
describe 'When asking for a note on merge request diff' do
let(:merge_request) { create(:merge_request) }
let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) }
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
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)
it 'returns the note url' do
expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
end
end
describe 'When asking for a note on project snippet' do
let(:snippet) { create(:project_snippet) }
let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) }
let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) }
context 'on 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
context 'on another object' do
it 'returns a proper URL' do
project = build_stubbed(:project)
it 'returns the note url' do
expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
expect { described_class.build(project) }.
to raise_error(NotImplementedError, 'No URL builder defined for Project')
end
end
end
end
end
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
before do
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
# the source branch. This seems like a reasonable compromise, in comparison with
......@@ -204,17 +204,24 @@ describe Issue, models: true do
end
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
it "selects the right branches when there is a referenced merge request" do
merge_request = create(:merge_request, { description: "Closes ##{subject.iid}",
source_project: subject.project,
source_branch: "branch-#{subject.iid}" })
source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user)
expect(subject.referenced_merge_requests).to_not be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
it 'excludes stable branches from the related branches' do
allow(subject.project.repository).to receive(:branch_names).
and_return(["#{subject.iid}-0-stable"])
expect(subject.related_branches(user)).to eq []
end
end
it_behaves_like 'an editable mentionable' do
......@@ -231,12 +238,12 @@ describe Issue, models: true do
describe "#to_branch_name" do
let(:issue) { create(:issue, title: 'testing-issue') }
it "ends with the issue iid" do
expect(issue.to_branch_name).to match /-#{issue.iid}\z/
it 'starts with the issue iid' do
expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/
end
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
it "does not contain the issue title if confidential" do
......
......@@ -21,74 +21,232 @@
require 'spec_helper'
describe BambooService, models: true do
describe "Associations" do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
describe 'Validations' do
describe '#bamboo_url' do
it 'does not validate the presence of bamboo_url if service is not active' do
bamboo_service = service
bamboo_service.active = false
context "when a password was previously set" do
before do
@bamboo_service = BambooService.create(
project: create(:project),
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
password: "password"
}
)
expect(bamboo_service).not_to validate_presence_of(:bamboo_url)
end
it 'validates the presence of bamboo_url if service is active' do
bamboo_service = service
bamboo_service.active = true
expect(bamboo_service).to validate_presence_of(:bamboo_url)
end
end
describe '#build_key' do
it 'does not validate the presence of build_key if service is not active' do
bamboo_service = service
bamboo_service.active = false
expect(bamboo_service).not_to validate_presence_of(:build_key)
end
it "reset password if url changed" do
@bamboo_service.bamboo_url = 'http://gitlab1.com'
@bamboo_service.save
expect(@bamboo_service.password).to be_nil
it 'validates the presence of build_key if service is active' do
bamboo_service = service
bamboo_service.active = true
expect(bamboo_service).to validate_presence_of(:build_key)
end
end
describe '#username' do
it 'does not validate the presence of username if service is not active' do
bamboo_service = service
bamboo_service.active = false
expect(bamboo_service).not_to validate_presence_of(:username)
end
it "does not reset password if username changed" do
@bamboo_service.username = "some_name"
@bamboo_service.save
expect(@bamboo_service.password).to eq("password")
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
describe '#password' do
it 'does not validate the presence of password if service is not active' do
bamboo_service = service
bamboo_service.active = false
expect(bamboo_service).not_to validate_presence_of(:password)
end
it 'does not validate the presence of password if username is nil' do
bamboo_service = service
bamboo_service.active = true
bamboo_service.username = nil
expect(bamboo_service).not_to validate_presence_of(:password)
end
it 'validates the presence of password if service is active and username is present' do
bamboo_service = service
bamboo_service.active = true
bamboo_service.username = 'john'
expect(bamboo_service).to validate_presence_of(:password)
end
end
end
describe 'Callbacks' do
describe 'before_update :reset_password' do
context 'when a password was previously set' do
it 'resets password if url changed' do
bamboo_service = service
bamboo_service.bamboo_url = 'http://gitlab1.com'
bamboo_service.save
expect(bamboo_service.password).to be_nil
end
it 'does not reset password if username changed' do
bamboo_service = service
bamboo_service.username = 'some_name'
bamboo_service.save
expect(bamboo_service.password).to eq('password')
end
it "does not reset password if new url is set together with password, even if it's the same password" do
@bamboo_service.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")
bamboo_service = service
bamboo_service.bamboo_url = 'http://gitlab_edited.com'
bamboo_service.password = 'password'
bamboo_service.save
expect(bamboo_service.password).to eq('password')
expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
end
end
it "should reset password if url changed, even if setter called multiple times" do
@bamboo_service.bamboo_url = 'http://gitlab1.com'
@bamboo_service.bamboo_url = 'http://gitlab1.com'
@bamboo_service.save
expect(@bamboo_service.password).to be_nil
it 'saves password if new url is set together with password when no password was previously set' do
bamboo_service = service
bamboo_service.password = nil
bamboo_service.bamboo_url = 'http://gitlab_edited.com'
bamboo_service.password = 'password'
bamboo_service.save
expect(bamboo_service.password).to eq('password')
expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
end
end
end
describe '#build_page' do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
end
it 'returns a specific URL when response has no results' do
stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
end
it 'returns a build URL when bamboo_url has no trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
end
it 'returns a build URL when bamboo_url has a trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
end
end
describe '#commit_status' do
it 'sets commit status to :error when status is 500' do
stub_request(status: 500)
expect(service.commit_status('123', 'unused')).to eq(:error)
end
it 'sets commit status to "pending" when status is 404' do
stub_request(status: 404)
expect(service.commit_status('123', 'unused')).to eq('pending')
end
it 'sets commit status to "pending" when response has no results' do
stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
expect(service.commit_status('123', 'unused')).to eq('pending')
end
context "when no password was previously set" do
before do
@bamboo_service = BambooService.create(
project: create(:project),
it 'sets commit status to "success" when build state contains Success' do
stub_request(build_state: 'YAY Success!')
expect(service.commit_status('123', 'unused')).to eq('success')
end
it 'sets commit status to "failed" when build state contains Failed' do
stub_request(build_state: 'NO Failed!')
expect(service.commit_status('123', 'unused')).to eq('failed')
end
it 'sets commit status to "pending" when build state contains Pending' do
stub_request(build_state: 'NO Pending!')
expect(service.commit_status('123', 'unused')).to eq('pending')
end
it 'sets commit status to :error when build state is unknown' do
stub_request(build_state: 'FOO BAR!')
expect(service.commit_status('123', 'unused')).to eq(:error)
end
end
def service(bamboo_url: 'http://gitlab.com')
described_class.create(
project: build_stubbed(:empty_project),
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic'
bamboo_url: bamboo_url,
username: 'mic',
password: 'password',
build_key: 'foo'
}
)
end
it "saves password if new url is set together with password" do
@bamboo_service.bamboo_url = 'http://gitlab_edited.com'
@bamboo_service.password = 'password'
@bamboo_service.save
expect(@bamboo_service.password).to eq("password")
expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com")
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}"}}}})
end
WebMock.stub_request(:get, bamboo_full_url).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
)
end
end
......@@ -3,9 +3,10 @@ require 'spec_helper'
describe BuildsEmailService do
let(:build) { create(:ci_build) }
let(:data) { Gitlab::BuildDataBuilder.build(build) }
let(:service) { BuildsEmailService.new }
let!(:project) { create(:project, :public, ci_id: 1) }
let(:service) { described_class.new(project: project, active: true) }
describe :execute do
describe '#execute' do
it 'sends email' do
service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed'
......@@ -40,4 +41,36 @@ describe BuildsEmailService do
service.execute(data)
end
end
describe 'validations' do
context 'when pusher is not added' do
before { service.add_pusher = false }
it 'does not allow empty recipient input' do
service.recipients = ''
expect(service.valid?).to be false
end
it 'does allow non-empty recipient input' do
service.recipients = 'test@example.com'
expect(service.valid?).to be true
end
end
context 'when pusher is added' do
before { service.add_pusher = true }
it 'does allow empty recipient input' do
service.recipients = ''
expect(service.valid?).to be true
end
it 'does allow non-empty recipient input' do
service.recipients = 'test@example.com'
expect(service.valid?).to be true
end
end
end
end
......@@ -21,73 +21,220 @@
require 'spec_helper'
describe TeamcityService, models: true do
describe "Associations" do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
describe 'Validations' do
describe '#teamcity_url' do
it 'does not validate the presence of teamcity_url if service is not active' do
teamcity_service = service
teamcity_service.active = false
context "when a password was previously set" do
before do
@teamcity_service = TeamcityService.create(
project: create(:project),
properties: {
teamcity_url: 'http://gitlab.com',
username: 'mic',
password: "password"
}
)
expect(teamcity_service).not_to validate_presence_of(:teamcity_url)
end
it 'validates the presence of teamcity_url if service is active' do
teamcity_service = service
teamcity_service.active = true
expect(teamcity_service).to validate_presence_of(:teamcity_url)
end
end
describe '#build_type' do
it 'does not validate the presence of build_type if service is not active' do
teamcity_service = service
teamcity_service.active = false
expect(teamcity_service).not_to validate_presence_of(:build_type)
end
it 'validates the presence of build_type if service is active' do
teamcity_service = service
teamcity_service.active = true
expect(teamcity_service).to validate_presence_of(:build_type)
end
end
describe '#username' do
it 'does not validate the presence of username if service is not active' do
teamcity_service = service
teamcity_service.active = false
expect(teamcity_service).not_to validate_presence_of(:username)
end
it 'does not validate the presence of username if username is nil' do
teamcity_service = service
teamcity_service.active = true
teamcity_service.password = nil
expect(teamcity_service).not_to validate_presence_of(:username)
end
it 'validates the presence of username if service is active and username is present' do
teamcity_service = service
teamcity_service.active = true
teamcity_service.password = 'secret'
expect(teamcity_service).to validate_presence_of(:username)
end
end
describe '#password' do
it 'does not validate the presence of password if service is not active' do
teamcity_service = service
teamcity_service.active = false
expect(teamcity_service).not_to validate_presence_of(:password)
end
it 'does not validate the presence of password if username is nil' do
teamcity_service = service
teamcity_service.active = true
teamcity_service.username = nil
expect(teamcity_service).not_to validate_presence_of(:password)
end
it 'validates the presence of password if service is active and username is present' do
teamcity_service = service
teamcity_service.active = true
teamcity_service.username = 'john'
expect(teamcity_service).to validate_presence_of(:password)
end
end
end
describe 'Callbacks' do
describe 'before_update :reset_password' do
context 'when a password was previously set' do
it 'resets password if url changed' do
teamcity_service = service
teamcity_service.teamcity_url = 'http://gitlab1.com'
teamcity_service.save
it "reset password if url changed" do
@teamcity_service.teamcity_url = 'http://gitlab1.com'
@teamcity_service.save
expect(@teamcity_service.password).to be_nil
expect(teamcity_service.password).to be_nil
end
it "does not reset password if username changed" do
@teamcity_service.username = "some_name"
@teamcity_service.save
expect(@teamcity_service.password).to eq("password")
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.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")
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 "should reset password if url changed, even if setter called multiple times" do
@teamcity_service.teamcity_url = 'http://gitlab1.com'
@teamcity_service.teamcity_url = 'http://gitlab1.com'
@teamcity_service.save
expect(@teamcity_service.password).to be_nil
it '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
context "when no password was previously set" do
before do
@teamcity_service = TeamcityService.create(
project: create(:project),
def service(teamcity_url: 'http://gitlab.com')
described_class.create(
project: build_stubbed(:empty_project),
properties: {
teamcity_url: 'http://gitlab.com',
username: 'mic'
teamcity_url: teamcity_url,
username: 'mic',
password: 'password',
build_type: 'foo'
}
)
end
it "saves password if new url is set together with password" do
@teamcity_service.teamcity_url = 'http://gitlab_edited.com'
@teamcity_service.password = 'password'
@teamcity_service.save
expect(@teamcity_service.password).to eq("password")
expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com")
end
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
......@@ -393,6 +393,8 @@ describe Repository, models: true do
describe '#expire_cache' do
it 'expires all caches' do
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
end
......
......@@ -42,9 +42,10 @@ describe API::API, api: true do
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)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end
end
end
......@@ -165,12 +166,13 @@ describe API::API, api: true do
end
end
describe "DELETE /groups/:id/members/:user_id" do
context "when not a member of the group" do
describe 'DELETE /groups/:id/members/:user_id' do
context 'when not a member of the group' do
it "should not delete guest's membership of group_with_members" do
random_user = create(: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
......
......@@ -61,7 +61,8 @@ describe API::API, api: true do
it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end
end
......@@ -92,9 +93,54 @@ describe API::API, api: true do
it 'should not return a group not attached to user1' do
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)
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
describe "GET /groups/:id/projects" do
......@@ -113,7 +159,8 @@ describe API::API, api: true do
it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end
end
......@@ -145,7 +192,8 @@ describe API::API, api: true do
it 'should not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end
end
end
......@@ -203,7 +251,8 @@ describe API::API, api: true do
it "should not remove a group not attached to user1" do
delete api("/groups/#{group2.id}", user1)
expect(response.status).to eq(403)
expect(response.status).to eq(404)
end
end
......
......@@ -3,11 +3,12 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
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
create :closed_issue,
author: user,
......@@ -320,13 +321,13 @@ describe API::API, api: true do
end
context 'when an admin or owner makes the request' do
it "accepts the creation date to be set" do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago
title: 'new issue', labels: 'label, label2', created_at: creation_time
expect(response.status).to eq(201)
# this take about a second, so probably not equal
expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
end
......@@ -477,6 +478,18 @@ describe API::API, api: true do
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
end
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time)
end
end
end
describe "DELETE /projects/:id/issues/:issue_id" do
......@@ -501,4 +514,114 @@ describe API::API, api: true do
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
......@@ -516,6 +516,48 @@ describe API::API, api: true do
end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
it 'subscribes to a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
expect(response.status).to eq(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
expect(response.status).to eq(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user)
expect(response.status).to eq(404)
end
end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
it 'unsubscribes from a merge request' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
expect(response.status).to eq(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
expect(response.status).to eq(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user)
expect(response.status).to eq(404)
end
end
def mr_with_later_created_and_updated_at_time
merge_request
merge_request.created_at += 1.hour
......
......@@ -158,6 +158,19 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
expect(response.status).to eq(401)
end
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'hi!', created_at: creation_time
expect(response.status).to eq(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
end
context "when noteable is a Snippet" do
......
......@@ -1020,6 +1020,54 @@ describe API::API, api: true do
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
context 'when authenticated as user' do
it 'should remove project' do
......
......@@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do
def transfer_project(project, user, new_namespace)
Projects::TransferService.new(project, user).execute(new_namespace)
end
context 'visibility level' do
let(:internal_group) { create(:group, :internal) }
before { internal_group.add_owner(user) }
context 'when namespace visibility level < project visibility level' do
let(:public_project) { create(:project, :public, namespace: user.namespace) }
before { transfer_project(public_project, user, internal_group) }
it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) }
end
context 'when namespace visibility level > project visibility level' do
let(:private_project) { create(:project, :private, namespace: user.namespace) }
before { transfer_project(private_project, user, internal_group) }
it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
end
end
end
......@@ -4,6 +4,9 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" }
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:project) { create(:project) }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
context "as a resque worker" do
it "reponds to #perform" do
......@@ -11,11 +14,43 @@ describe PostReceive do
end
end
context "webhook" do
let(:project) { create(:project) }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
describe "#process_project_changes" do
before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
end
context "branches" do
let(:changes) { "123456 789012 refs/heads/tést" }
it "should call GitTagPushService" do
expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
expect_any_instance_of(GitTagPushService).not_to receive(:execute)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
context "tags" do
let(:changes) { "123456 789012 refs/tags/tag" }
it "should call GitTagPushService" do
expect_any_instance_of(GitPushService).not_to receive(:execute)
expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
context "merge-requests" do
let(:changes) { "123456 789012 refs/merge-requests/123" }
it "should not call any of the services" do
expect_any_instance_of(GitPushService).not_to receive(:execute)
expect_any_instance_of(GitTagPushService).not_to receive(:execute)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
end
context "webhook" do
it "fetches the correct project" do
expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
......
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