Commit f07316f2 authored by Jacob Vosmaer's avatar Jacob Vosmaer

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into auto-fsck

parents 6f6d2d0a 3463ffde
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)
- All service classes (those residing in app/services) are now instrumented (Yorick Peterse)
- Developers can now add custom tags to transactions (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
- 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)
- Expose project badges in project settings
......@@ -13,24 +17,31 @@ v 8.7.0 (unreleased)
- 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 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
- Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
- API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling)
- API: Ability to star and unstar a project (Robert Schilling)
- Add default scope to projects to exclude projects pending deletion
- Allow to close merge requests which source projects(forks) are deleted.
- Ensure empty recipients are rejected in BuildsEmailService
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups
- Fix high CPU usage when PostReceive receives refs/merge-requests/<id>
- Hide `Create a group` help block when creating a new project in a group
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Decouple membership and notifications
- Fix creation of merge requests for orphaned branches (Stan Hu)
- API: Ability to retrieve a single tag (Robert Schilling)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
......@@ -38,13 +49,16 @@ v 8.7.0 (unreleased)
- Fix admin/projects when using visibility levels on search (PotHix)
- Build status notifications
- API: Expose user location (Robert Schilling)
- API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
- API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
v 8.6.6
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- Project switcher uses new dropdown styling
v 8.6.5
- Fix importing from GitHub Enterprise. !3529
......
......@@ -285,9 +285,9 @@ group :development, :test do
gem 'teaspoon', '~> 1.1.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.6.4'
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.38.0', require: false
......
......@@ -769,10 +769,10 @@ GEM
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
spring (1.6.4)
spring (1.7.1)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-spinach (1.0.0)
spring-commands-spinach (1.1.0)
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
......@@ -1030,9 +1030,9 @@ DEPENDENCIES
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.6.4)
spring (~> 1.7.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0)
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.6.0)
state_machines-activerecord (~> 0.3.0)
......
......@@ -22,7 +22,17 @@
#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap
#= require bootstrap/affix
#= require bootstrap/alert
#= require bootstrap/button
#= require bootstrap/collapse
#= require bootstrap/dropdown
#= require bootstrap/modal
#= require bootstrap/scrollspy
#= require bootstrap/tab
#= require bootstrap/transition
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2
#= require raphael
#= require g.raphael
......@@ -41,6 +51,7 @@
#= require shortcuts_issuable
#= require shortcuts_network
#= require jquery.nicescroll
#= require date.format
#= require_tree .
#= require fuzzaldrin-plus
#= require cropper
......@@ -163,7 +174,7 @@ $ ->
$('.trigger-submit').on 'change', ->
$(@).parents('form').submit()
$('abbr.timeago, .js-timeago').timeago()
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false)
# Flash
if (flash = $(".flash-container")).length > 0
......
......@@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
e.preventDefault()
$form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable()
$submit_button = $form.find('input[type=submit], button[type=submit]')
return if $submit_button.attr('disabled')
$submit_button.disable()
$form.submit()
# If the user tabs to a submit button on a `js-quick-submit` form, display a
......
......@@ -122,7 +122,9 @@ class GitLabDropdown
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
constructor: (@el, @options) ->
@dropdown = $(@el).parent()
self = @
selector = $(@el).data "target"
@dropdown = if selector? then $(selector) else $(@el).parent()
# Set Defaults
{
......
......@@ -218,7 +218,7 @@ class @LabelsSelect
selectable: true
toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label'
if selected and selected.title?
selected.title
else
defaultLabel
......
((w) ->
w.gl ?= {}
w.gl.utils ?= {}
w.gl.utils.formatDate = (datetime) ->
dateFormat(datetime, 'mmm d, yyyy h:MMtt Z')
w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) ->
$timeagoEls.each( ->
$el = $(@)
$el.attr('title', gl.utils.formatDate($el.attr('datetime')))
)
$timeagoEls.timeago() if setTimeago
) window
......@@ -2,6 +2,11 @@
notificationGranted = (message, opts, onclick) ->
notification = new Notification(message, opts)
# Hide the notification after X amount of seconds
setTimeout ->
notification.close()
, 8000
if onclick
notification.onclick = onclick
......
......@@ -142,7 +142,7 @@ class @MergeRequestTabs
url: "#{source}.json"
success: (data) =>
document.querySelector("div#commits").innerHTML = data.html
$('.js-timeago').timeago()
gl.utils.localTimeAgo($('.js-timeago', 'div#commits'))
@commitsLoaded = true
@scrollToElement("#commits")
......@@ -153,7 +153,7 @@ class @MergeRequestTabs
url: "#{source}.json" + @_location.search
success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html
$('.js-timeago').timeago()
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
$('div#diffs .js-syntax-highlight').syntaxHighlight()
@expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
......@@ -166,7 +166,7 @@ class @MergeRequestTabs
url: "#{source}.json"
success: (data) =>
document.querySelector("div#builds").innerHTML = data.html
$('.js-timeago').timeago()
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'))
@buildsLoaded = true
@scrollToElement("#builds")
......
......@@ -12,10 +12,19 @@ class @MergeRequestWidget
@readyForCICheck = true
clearInterval @fetchBuildStatusInterval
@clearEventListeners()
@addEventListeners()
@pollCIStatus()
notifyPermissions()
setOpts: (@opts) ->
clearEventListeners: ->
$(document).off 'page:change.merge_request'
addEventListeners: ->
$(document).on 'page:change.merge_request', =>
if $('body').data('page') isnt 'projects:merge_requests:show'
clearInterval @fetchBuildStatusInterval
@clearEventListeners()
mergeInProgress: (deleteSourceBranch = false)->
$.ajax
......@@ -38,7 +47,7 @@ class @MergeRequestWidget
$('.mr-state-widget').replaceWith(data)
ciLabelForStatus: (status) ->
if status == 'success'
if status is 'success'
'passed'
else
status
......@@ -67,18 +76,28 @@ class @MergeRequestWidget
@opts.ci_status = data.status
return
if data.status isnt @opts.ci_status
if data.status isnt @opts.ci_status and data.status?
@showCIStatus data.status
if data.coverage
@showCICoverage data.coverage
if showNotification
message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status))
status = @ciLabelForStatus(data.status)
if status is "preparing"
title = @opts.ci_title.preparing
status = status.charAt(0).toUpperCase() + status.slice(1);
message = @opts.ci_message.preparing.replace('{{status}}', status)
else
title = @opts.ci_title.normal
message = @opts.ci_message.normal.replace('{{status}}', status)
title = title.replace('{{status}}', status)
message = message.replace('{{sha}}', data.sha)
message = message.replace('{{title}}', data.title)
notify(
"Build #{@ciLabelForStatus(data.status)}",
title,
message,
@opts.gitlab_icon,
->
......@@ -98,6 +117,8 @@ class @MergeRequestWidget
@setMergeButtonClass('btn-danger')
when "running", "pending"
@setMergeButtonClass('btn-warning')
when "success"
@setMergeButtonClass('btn-create')
else
$('.ci_widget.ci-error').show()
@setMergeButtonClass('btn-danger')
......@@ -107,4 +128,6 @@ class @MergeRequestWidget
$('.ci_widget:visible .ci-coverage').text(text)
setMergeButtonClass: (css_class) ->
$('.accept_merge_request').removeClass("btn-create").addClass(css_class)
$('.accept_merge_request')
.removeClass('btn-danger btn-warning btn-create')
.addClass(css_class)
......@@ -163,9 +163,15 @@ class @Notes
else if @isNewNote(note)
@note_ids.push(note.id)
$('ul.main-notes-list')
$notesList = $('ul.main-notes-list')
$notesList
.append(note.html)
.syntaxHighlight()
# Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false)
@initTaskList()
@updateNotesCount(1)
......@@ -217,6 +223,8 @@ class @Notes
# append new note to all matching discussions
discussionContainer.append note_html
gl.utils.localTimeAgo($('.js-timeago', note_html), false)
@updateNotesCount(1)
###
......@@ -345,7 +353,9 @@ class @Notes
updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html)
$('.js-timeago', $html).timeago()
gl.utils.localTimeAgo($('.js-timeago', $html))
$html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable')
......
......@@ -18,8 +18,11 @@ class @Profile
$(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide()
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
$('.update-notifications').on 'ajax:success', (e, data) ->
if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@bindEvents()
......
......@@ -37,19 +37,20 @@ class @Project
$('.update-notification').on 'click', (e) ->
e.preventDefault()
notification_level = $(@).data 'notification-level'
$('#notification_level').val(notification_level)
label = $(@).data 'notification-title'
$('#notification_setting_level').val(notification_level)
$('#notification-form').submit()
label = null
switch notification_level
when 0 then label = ' Disabled '
when 1 then label = ' Participating '
when 2 then label = ' Watching '
when 3 then label = ' Global '
when 4 then label = ' On Mention '
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active'
$('#notification-form').on 'ajax:success', (e, data) ->
if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@projectSelectDropdown()
projectSelectDropdown: ->
......
class @ProjectSelect
constructor: ->
$('.js-projects-dropdown-toggle').each (i, dropdown) ->
$dropdown = $(dropdown)
$dropdown.glDropdown(
filterable: true
filterRemote: true
search:
fields: ['name_with_namespace']
data: (term, callback) ->
finalCallback = (projects) ->
callback projects
if @includeGroups
projectsCallback = (projects) ->
groupsCallback = (groups) ->
data = groups.concat(projects)
finalCallback(data)
Api.groups term, false, groupsCallback
else
projectsCallback = finalCallback
if @groupId
Api.groupProjects @groupId, term, projectsCallback
else
Api.projects term, @orderBy, projectsCallback
url: (project) ->
project.web_url
text: (project) ->
project.name_with_namespace
)
$('.ajax-project-select').each (i, select) ->
@groupId = $(select).data('group-id')
@includeGroups = $(select).data('include-groups')
......
......@@ -59,6 +59,8 @@ class @Todos
goToTodoUrl: (e)->
todoLink = $(this).data('url')
return unless todoLink
if e.metaKey
e.preventDefault()
window.open(todoLink,'_blank')
......
......@@ -69,6 +69,7 @@ header {
}
.header-content {
position: relative;
height: $header-height;
padding-right: 20px;
......@@ -76,6 +77,10 @@ header {
padding-right: 0;
}
.dropdown-menu {
margin-top: -5px;
}
.title {
margin: 0;
font-size: 19px;
......
......@@ -58,12 +58,12 @@
.nav-search {
display: inline-block;
width: 50%;
width: 100%;
padding: 11px 0;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
width: 100%;
@media (min-width: $screen-sm-min) {
width: 50%;
}
}
......
......@@ -43,7 +43,6 @@
@import "bootstrap/modals";
@import "bootstrap/tooltip";
@import "bootstrap/popovers";
@import "bootstrap/carousel";
// Utility classes
.clearfix {
......
......@@ -28,6 +28,7 @@ $gl-link-color: #3084bb;
$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
$gl-icon-color: $gl-placeholder-color;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-header-color: $gl-title-color;
......
......@@ -41,8 +41,17 @@
word-wrap: break-word;
.md {
color: #7f8fa4;
color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {
color: $gl-text-color;
font-size: inherit;
}
iframe.twitter-share-button {
vertical-align: bottom;
}
}
pre {
......
......@@ -263,6 +263,12 @@
}
}
.dropdown-content {
a:hover {
color: inherit;
}
}
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
......
......@@ -142,6 +142,7 @@
overflow: hidden;
font-size: 90%;
margin: 0 3px;
word-break: break-all;
}
.mr-list {
......
......@@ -71,12 +71,24 @@
border-color: $focus-border-color;
}
}
p {
code {
white-space: normal;
}
pre {
code {
white-space: pre;
}
}
}
}
}
.discussion-form {
padding: $gl-padding-top $gl-padding;
background-color: #fff;
background-color: $white-light;
}
.note-edit-form {
......
......@@ -81,11 +81,17 @@ ul.notes {
@include md-typography;
// On diffs code should wrap nicely and not overflow
p {
code {
white-space: normal;
}
pre {
code {
white-space: pre;
}
}
}
// Reset ul style types since we're nested inside a ul already
& > ul {
......@@ -112,6 +118,10 @@ ul.notes {
margin: 10px 0;
}
}
a {
word-break: break-all;
}
}
.note-header {
......@@ -127,7 +137,7 @@ ul.notes {
margin-right: 10px;
}
.line_content {
white-space: pre-wrap;
white-space: pre;
}
}
......@@ -145,19 +155,27 @@ ul.notes {
background: $background-color;
color: $text-color;
}
&.notes_line2 {
text-align: center;
padding: 10px 0;
border-left: 1px solid #ddd !important;
}
&.notes_content {
background-color: #fff;
background-color: $background-color;
border-width: 1px 0;
padding: 0;
vertical-align: top;
white-space: normal;
&.parallel {
border-width: 1px;
}
.notes {
background-color: $white-light;
}
}
}
}
......
......@@ -34,6 +34,11 @@
color: #7f8fa4;
font-size: $gl-font-size;
.label {
color: $gl-text-color;
font-size: inherit;
}
p {
color: #5c5d5e;
}
......
class Groups::NotificationSettingsController < Groups::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(group)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
......@@ -60,6 +60,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
continue_login_process
end
rescue Gitlab::OAuth::SignupDisabledError
handle_signup_error
end
def omniauth_error
......@@ -92,16 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
continue_login_process
end
rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if current_application_settings.signup_enabled?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
flash[:notice] = message
redirect_to new_user_session_path
handle_signup_error
end
def handle_service_ticket provider, ticket
......@@ -122,6 +115,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
def handle_signup_error
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if current_application_settings.signup_enabled?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
flash[:notice] = message
redirect_to new_user_session_path
end
def oauth
@oauth ||= request.env['omniauth.auth']
end
......
class Profiles::NotificationsController < Profiles::ApplicationController
def show
@user = current_user
@notification = current_user.notification
@project_members = current_user.project_members
@group_members = current_user.group_members
@group_notifications = current_user.notification_settings.for_groups
@project_notifications = current_user.notification_settings.for_projects
end
def update
type = params[:notification_type]
@saved = if type == 'global'
current_user.update_attributes(user_params)
elsif type == 'group'
group_member = current_user.group_members.find(params[:notification_id])
group_member.notification_level = params[:notification_level]
group_member.save
else
project_member = current_user.project_members.find(params[:notification_id])
project_member.notification_level = params[:notification_level]
project_member.save
end
respond_to do |format|
format.html do
if @saved
if current_user.update_attributes(user_params)
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
......@@ -32,10 +15,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
redirect_back_or_default(default: profile_notifications_path)
end
format.js
end
end
def user_params
params.require(:user).permit(:notification_email, :notification_level)
end
......
......@@ -237,6 +237,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
status = "preparing" if status.nil?
response = {
title: merge_request.title,
sha: merge_request.last_commit_short_sha,
......
class Projects::NotificationSettingsController < Projects::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(project)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
......@@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
RepositoryArchiveCacheWorker.perform_async
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
head :ok
rescue => ex
......
......@@ -101,14 +101,18 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
if @project.repository_exists?
if @project.empty_repo?
render 'projects/empty'
else
if current_user
@membership = @project.team.find_member(current_user.id)
if @membership
@notification_setting = current_user.notification_settings_for(@project)
end
end
if @project.repository_exists?
if @project.empty_repo?
render 'projects/empty'
else
render :show
end
else
......
......@@ -184,7 +184,7 @@ module ApplicationHelper
element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
datetime: time.to_time.getutc.iso8601,
title: time.in_time_zone.to_s(:medium),
title: time.to_time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js
......
module NotificationsHelper
include IconsHelper
def notification_icon(notification)
if notification.disabled?
icon('volume-off', class: 'ns-mute')
elsif notification.participating?
icon('volume-down', class: 'ns-part')
elsif notification.watch?
icon('volume-up', class: 'ns-watch')
else
icon('circle-o', class: 'ns-default')
def notification_icon_class(level)
case level.to_sym
when :disabled
'microphone-slash'
when :participating
'volume-up'
when :watch
'eye'
when :mention
'at'
when :global
'globe'
end
end
def notification_list_item(notification_level, user_membership)
case notification_level
when Notification::N_DISABLED
update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash')
when Notification::N_PARTICIPATING
update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up')
when Notification::N_WATCH
update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye')
when Notification::N_MENTION
update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at')
when Notification::N_GLOBAL
update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe')
else
# do nothing
end
def notification_icon(level, text = nil)
icon("#{notification_icon_class(level)} fw", text: text)
end
def update_notification_link(notification_level, user_membership, title, icon)
content_tag(:li, class: active_level_for(user_membership, notification_level)) do
link_to '#', class: 'update-notification', data: { notification_level: notification_level } do
icon("#{icon} fw", text: title)
end
def notification_title(level)
case level.to_sym
when :participating
'Participate'
when :mention
'On mention'
else
level.to_s.titlecase
end
end
def notification_label(user_membership)
Notification.new(user_membership).to_s
end
def notification_list_item(level, setting)
title = notification_title(level)
data = {
notification_level: level,
notification_title: title
}
def active_level_for(user_membership, level)
'active' if user_membership.notification_level == level
content_tag(:li, class: ('active' if setting.level == level)) do
link_to '#', class: 'update-notification', data: data do
notification_icon(level, title)
end
end
end
end
......@@ -65,21 +65,14 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner))
end
project_link = link_to project_path(project), { class: "project-item-select-holder" } do
link_output = simple_sanitize(project.name)
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user
link_output += project_select_tag :project_path,
class: "project-item-select js-projects-dropdown",
data: { include_groups: false, order_by: 'last_activity_at' }
project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
end
link_output
end
project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user
full_title = namespace_link + ' / ' + project_link
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
full_title = "#{namespace_link} / #{project_link}".html_safe
full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
full_title
end
......
......@@ -20,6 +20,8 @@ module TodosHelper
end
def todo_target_path(todo)
return unless todo.target.present?
anchor = dom_id(todo.note) if todo.note.present?
if todo.for_commit?
......
......@@ -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: commit_url,
author: {
name: author_name,
email: author_email
......@@ -170,6 +168,10 @@ class Commit
data
end
def commit_url
project.present? ? "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{id}" : ""
end
# Discover issues should be closed when this commit is pushed to a project's
# default branch.
def closes_issues(current_user = self.committer)
......
# == Notifiable concern
#
# Contains notification functionality
#
module Notifiable
extend ActiveSupport::Concern
included do
validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true
end
def notification
@notification ||= Notification.new(self)
end
end
......@@ -27,6 +27,7 @@ class Group < Namespace
has_many :users, through: :group_members
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
......
......@@ -19,7 +19,6 @@
class Member < ActiveRecord::Base
include Sortable
include Notifiable
include Gitlab::Access
attr_accessor :raw_invite_token
......@@ -56,12 +55,15 @@ class Member < ActiveRecord::Base
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite?
after_create :post_create_hook, unless: :invite?
after_update :post_update_hook, unless: :invite?
after_destroy :post_destroy_hook, unless: :invite?
delegate :name, :username, :email, to: :user, prefix: true
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
def find_by_invite_token(invite_token)
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
......@@ -160,6 +162,14 @@ class Member < ActiveRecord::Base
send_invite
end
def create_notification_setting
user.notification_settings.find_or_create_for(source)
end
def notification_setting
@notification_setting ||= user.notification_settings_for(source)
end
private
def send_invite
......
......@@ -24,7 +24,6 @@ class GroupMember < Member
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
default_value_for :notification_level, Notification::N_GLOBAL
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
......
......@@ -27,7 +27,6 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
default_value_for :notification_level, Notification::N_GLOBAL
validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) }
......
......@@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches
validate :validate_branches, unless: :allow_broken
validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
......@@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base
end
if opened? || reopened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
errors.add :validate_branches,
......@@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base
def hook_attrs
attrs = {
source: source_project.hook_attrs,
source: source_project.try(:hook_attrs),
target: target_project.hook_attrs,
last_commit: nil,
work_in_progress: work_in_progress?
......
class Notification
#
# Notification levels
#
N_DISABLED = 0
N_PARTICIPATING = 1
N_WATCH = 2
N_GLOBAL = 3
N_MENTION = 4
attr_accessor :target
class << self
def notification_levels
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH]
end
def options_with_labels
{
disabled: N_DISABLED,
participating: N_PARTICIPATING,
watch: N_WATCH,
mention: N_MENTION,
global: N_GLOBAL
}
end
def project_notification_levels
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL]
end
end
def initialize(target)
@target = target
end
def disabled?
target.notification_level == N_DISABLED
end
def participating?
target.notification_level == N_PARTICIPATING
end
def watch?
target.notification_level == N_WATCH
end
def global?
target.notification_level == N_GLOBAL
end
def mention?
target.notification_level == N_MENTION
end
def level
target.notification_level
end
def to_s
case level
when N_DISABLED
'Disabled'
when N_PARTICIPATING
'Participating'
when N_WATCH
'Watching'
when N_MENTION
'On mention'
when N_GLOBAL
'Global'
else
# do nothing
end
end
end
class NotificationSetting < ActiveRecord::Base
enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
default_value_for :level, NotificationSetting.levels[:global]
belongs_to :user
belongs_to :source, polymorphic: true
validates :user, presence: true
validates :source, presence: true
validates :level, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
scope :for_groups, -> { where(source_type: 'Namespace') }
scope :for_projects, -> { where(source_type: 'Project') }
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
unless setting.persisted?
setting.save
end
setting
end
end
......@@ -154,6 +154,7 @@ class Project < ActiveRecord::Base
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
......@@ -388,9 +389,15 @@ class Project < ActiveRecord::Base
def add_import_job
if forked?
RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
else
RepositoryImportWorker.perform_async(self.id)
job_id = RepositoryImportWorker.perform_async(self.id)
end
if job_id
Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
else
Rails.logger.error "Import job failed to start for #{path_with_namespace}"
end
end
......
......@@ -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)
......
......@@ -143,6 +143,7 @@ class User < ActiveRecord::Base
has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
#
# Validations
......@@ -157,7 +158,7 @@ class User < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validates :notification_level, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? }
......@@ -190,6 +191,13 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files]
# Notification level
# Note: When adding an option, it MUST go on the end of the array.
#
# TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
# Because user.notification_disabled? is much better than user.disabled?
enum notification_level: [:disabled, :participating, :watch, :global, :mention]
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true
......@@ -349,10 +357,6 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}"
end
def notification
@notification ||= Notification.new(self)
end
def generate_password
if self.force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(8)
......@@ -827,6 +831,10 @@ class User < ActiveRecord::Base
end
end
def notification_settings_for(source)
notification_settings.find_or_initialize_by(source: source)
end
private
def projects_union
......
......@@ -253,8 +253,8 @@ class NotificationService
def project_watchers(project)
project_members = project_member_notification(project)
users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL)
users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL)
users_with_project_level_global = project_member_notification(project, :global)
users_with_group_level_global = group_member_notification(project, :global)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
......@@ -264,18 +264,16 @@ class NotificationService
end
def project_member_notification(project, notification_level=nil)
project_members = project.project_members
if notification_level
project_members.where(notification_level: notification_level).pluck(:user_id)
project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
else
project_members.pluck(:user_id)
project.notification_settings.pluck(:user_id)
end
end
def group_member_notification(project, notification_level)
if project.group
project.group.group_members.where(notification_level: notification_level).pluck(:user_id)
project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
else
[]
end
......@@ -284,13 +282,13 @@ class NotificationService
def users_with_global_level_watch(ids)
User.where(
id: ids,
notification_level: Notification::N_WATCH
notification_level: NotificationSetting.levels[:watch]
).pluck(:id)
end
# Build a list of users based on project notifcation settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
users = project_member_notification(project, Notification::N_WATCH)
users = project_member_notification(project, :watch)
# If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id|
......@@ -304,7 +302,7 @@ class NotificationService
# Build a list of users based on group notification settings
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
uids = group_member_notification(project, Notification::N_WATCH)
uids = group_member_notification(project, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
......@@ -331,40 +329,46 @@ class NotificationService
# Remove users with disabled notifications from array
# Also remove duplications and nil recipients
def reject_muted_users(users, project = nil)
reject_users(users, :disabled?, project)
reject_users(users, :disabled, project)
end
# Remove users with notification level 'Mentioned'
def reject_mention_users(users, project = nil)
reject_users(users, :mention?, project)
reject_users(users, :mention, project)
end
# Reject users which method_name from notification object returns true.
# Reject users which has certain notification level
#
# Example:
# reject_users(users, :watch?, project)
# reject_users(users, :watch, project)
#
def reject_users(users, method_name, project = nil)
def reject_users(users, level, project = nil)
level = level.to_s
unless NotificationSetting.levels.keys.include?(level)
raise 'Invalid notification level'
end
users = users.to_a.compact.uniq
users = users.reject(&:blocked?)
users.reject do |user|
next user.notification.send(method_name) unless project
next user.notification_level == level unless project
member = project.project_members.find_by(user_id: user.id)
setting = user.notification_settings_for(project)
if !member && project.group
member = project.group.group_members.find_by(user_id: user.id)
if !setting && project.group
setting = user.notification_settings_for(project.group)
end
# reject users who globally set mention notification and has no membership
next user.notification.send(method_name) unless member
# reject users who globally set mention notification and has no setting per project/group
next user.notification_level == level unless setting
# reject users who set mention notification in project
next true if member.notification.send(method_name)
next true if setting.level == level
# reject users who have N_MENTION in project and disabled in global settings
member.notification.global? && user.notification.send(method_name)
# reject users who have mention level in project and disabled in global settings
setting.global? && user.notification_level == level
end
end
......
......@@ -26,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
......@@ -45,6 +45,8 @@
%h1.title= title
= yield :header_content
= render 'shared/outdated_browser'
- if @project && !@project.empty_repo?
......
......@@ -17,4 +17,12 @@
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
= dropdown_title("Go to a project")
= dropdown_filter("Search your projects")
= dropdown_content
= dropdown_loading
= render template: "layouts/application"
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
= notification_icon(current_user.notification_level)
- else
= notification_icon(setting.level)
%span.str-truncated
= link_to group.name, group_path(group)
.pull-right
= form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
= notification_icon(current_user.notification_level)
- else
= notification_icon(setting.level)
%span.str-truncated
= link_to_project(project)
.pull-right
= form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if notification.global?
= notification_icon(@notification)
- else
= notification_icon(notification)
%span.str-truncated
- if membership.kind_of? GroupMember
= link_to membership.group.name, membership.group
- else
= link_to_project(membership.project)
.pull-right
= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
= hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type')
= hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id')
= select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit'
- page_title "Notifications"
- header_title page_title, profile_notifications_path
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
= form_errors(@user)
%div
- if @user.errors.any?
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
= hidden_field_tag :notification_type, 'global'
.row
......@@ -16,35 +20,37 @@
.col-lg-9
%h5
Global notification settings
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
.form-group
= f.label :notification_email, class: "label-light"
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
.form-group
= f.label :notification_level, class: 'label-light'
.radio
= f.label :notification_level, value: Notification::N_DISABLED do
= f.radio_button :notification_level, Notification::N_DISABLED
= f.label :notification_level, value: :disabled do
= f.radio_button :notification_level, :disabled
.level-title
Disabled
%p You will not get any notifications via email
.radio
= f.label :notification_level, value: Notification::N_MENTION do
= f.radio_button :notification_level, Notification::N_MENTION
= f.label :notification_level, value: :mention do
= f.radio_button :notification_level, :mention
.level-title
On Mention
%p You will receive notifications only for comments in which you were @mentioned
.radio
= f.label :notification_level, value: Notification::N_PARTICIPATING do
= f.radio_button :notification_level, Notification::N_PARTICIPATING
= f.label :notification_level, value: :participating do
= f.radio_button :notification_level, :participating
.level-title
Participating
%p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
.radio
= f.label :notification_level, value: Notification::N_WATCH do
= f.radio_button :notification_level, Notification::N_WATCH
= f.label :notification_level, value: :watch do
= f.radio_button :notification_level, :watch
.level-title
Watch
%p You will receive notifications for any activity
......@@ -52,20 +58,17 @@
.prepend-top-default
= f.submit 'Update settings', class: "btn btn-create"
%hr
.col-lg-9.col-lg-push-3
%h5
Groups (#{@group_members.count})
Groups (#{@group_notifications.count})
%div
%ul.bordered-list
- @group_members.each do |group_member|
- notification = Notification.new(group_member)
= render 'settings', type: 'group', membership: group_member, notification: notification
- @group_notifications.each do |setting|
= render 'group_settings', setting: setting, group: setting.source
%h5
Projects (#{@project_members.count})
Projects (#{@project_notifications.count})
%p.account-well
To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there.
.append-bottom-default
%ul.bordered-list
- @project_members.each do |project_member|
- notification = Notification.new(project_member)
= render 'settings', type: 'project', membership: project_member, notification: notification
- @project_notifications.each do |setting|
= render 'project_settings', setting: setting, project: setting.source
- if @saved
:plain
new Flash("Notification settings saved", "notice")
- else
:plain
new Flash("Failed to save new settings", "alert")
- case @membership
- when ProjectMember
= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do
= hidden_field_tag :notification_type, 'project'
= hidden_field_tag :notification_id, @membership.id
= hidden_field_tag :notification_level
- if @notification_setting
= form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
= f.hidden_field :level
%span.dropdown
%a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
= icon('bell')
= notification_label(@membership)
= notification_title(@notification_setting.level)
= icon('angle-down')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- Notification.project_notification_levels.each do |level|
= notification_list_item(level, @membership)
- when GroupMember
.btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
- NotificationSetting.levels.each do |level|
= notification_list_item(level.first, @notification_setting)
= 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 gfm-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript
......
......@@ -8,20 +8,22 @@
= render 'projects/merge_requests/widget/locked'
:javascript
var merge_request_widget;
var opts = {
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "",
ci_message: "Build {{status}} for \"{{title}}\"",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
preparing: "{{status}} build for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
preparing: "{{status}} build",
normal: "Build {{status}}"
},
builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if(typeof merge_request_widget === 'undefined') {
merge_request_widget = new MergeRequestWidget(opts);
} else {
merge_request_widget.setOpts(opts);
}
......@@ -10,12 +10,12 @@
= "#{note.author.to_reference} commented"
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- if note_editable?(note)
.note-actions
- access = note.project.team.human_max_access(note.author.id)
- if access
%span.note-role
= access
- if note_editable?(note)
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
......
......@@ -6,7 +6,7 @@
.commit-message-container
.max-width-marker
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]),
(params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
......
- 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')
......
......@@ -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
......
......@@ -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
......@@ -163,6 +172,9 @@ production: &base
admin_email_worker:
cron: "0 0 * * *"
# Remove outdated repository archives
repository_archive_cache_worker:
cron: "0 * * * *"
#
# 2. GitLab CI settings
......@@ -311,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'] ||= []
#
......@@ -245,6 +247,9 @@ Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::B
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
......
Rails.application.config.action_dispatch.trusted_proxies =
[ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies)
......@@ -408,6 +408,7 @@ Rails.application.routes.draw do
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
resource :notification_setting, only: [:update]
end
end
......@@ -609,6 +610,7 @@ Rails.application.routes.draw do
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
resource :notification_setting, only: [:update]
resources :refs, only: [] do
collection do
......
class CreateNotificationSettings < ActiveRecord::Migration
def change
create_table :notification_settings do |t|
t.references :user, null: false
t.references :source, polymorphic: true, null: false
t.integer :level, default: 0, null: false
t.timestamps null: false
end
end
end
# This migration will create one row of NotificationSetting for each Member row
# It can take long time on big instances.
#
# This migration can be done online but with following effects:
# - during migration some users will receive notifications based on their global settings (project/group settings will be ignored)
# - its possible to get duplicate records for notification settings since we don't create uniq index yet
#
class MigrateNewNotificationSetting < ActiveRecord::Migration
def up
timestamp = Time.now
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
def down
execute "DELETE FROM notification_settings"
end
end
class AddNotificationSettingIndex < ActiveRecord::Migration
def change
add_index :notification_settings, :user_id
add_index :notification_settings, [:source_id, :source_type]
end
end
......@@ -638,6 +638,18 @@ ActiveRecord::Schema.define(version: 20160412140240) do
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
create_table "notification_settings", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "source_id", null: false
t.string "source_type", null: false
t.integer "level", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
create_table "oauth_access_grants", force: :cascade do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
......
......@@ -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).
......
......@@ -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.
......
......@@ -351,6 +351,61 @@ 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"
}
}
```
## Comments on issues
Comments are done via the [notes](notes.md) resource.
......@@ -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
......
......@@ -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:
......
......@@ -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:**
......
......@@ -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
......@@ -91,18 +91,18 @@ 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.
......
......@@ -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
`et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum-2`.
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
......@@ -7,3 +7,9 @@ Feature: Profile Notifications
Scenario: I visit notifications tab
When I visit profile notifications page
Then I should see global notifications settings
@javascript
Scenario: I edit Project Notifications
Given I visit profile notifications page
When I select Mention setting from dropdown
Then I should see Notification saved message
......@@ -9,4 +9,14 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
step 'I should see global notifications settings' do
expect(page).to have_content "Notifications"
end
step 'I select Mention setting from dropdown' do
select 'mention', from: 'notification_setting_level'
end
step 'I should see Notification saved message' do
page.within '.flash-container' do
expect(page).to have_content 'Notification settings saved'
end
end
end
This diff is collapsed.
......@@ -263,14 +263,19 @@ module API
expose :id, :path, :kind
end
class ProjectAccess < Grape::Entity
class Member < Grape::Entity
expose :access_level
expose :notification_level
expose :notification_level do |member, options|
if member.notification_setting
NotificationSetting.levels[member.notification_setting.level]
end
end
end
class GroupAccess < Grape::Entity
expose :access_level
expose :notification_level
class ProjectAccess < Member
end
class GroupAccess < Member
end
class ProjectService < Grape::Entity
......
......@@ -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)
......
......@@ -195,6 +195,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:
......
......@@ -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')
......
require 'gitlab/git'
module Gitlab
def self.com?
Gitlab.config.gitlab.url == 'https://gitlab.com'
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?
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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