Commit 9534e862 authored by Valery Sizov's avatar Valery Sizov

Merge remote-tracking branch 'origin/master' into ce_upstream

parents e84e66be 291678d6
Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased)
- Bump gollum-lib to 4.1.0 (Stan Hu)
- Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu)
- Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
- Handle and report SSL errors in Web hook test (Stan Hu)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
- Add rake tasks for git repository maintainance (Zeger-Jan van de Weg)
- Fix 500 error when update group member permission
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
- Recognize issue/MR/snippet/commit links as references
- Add ignore whitespace change option to commit view
- Fire update hook from GitLab
- Style warning about mentioning many people in a comment
- Fix: sort milestones by due date once again (Greg Smethells)
- Don't show project fork event as "imported"
- Add API endpoint to fetch merge request commits list
- Expose events API with comment information and author info
- Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583
- Fix 500 error when creating a merge request that removes a submodule
- Run custom Git hooks when branch is created or deleted.
- Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch
- Add languages page to graphs
- Block LDAP user when they are no longer found in the LDAP server
- Improve wording on project visibility levels (Zeger-Jan van de Weg)
v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu)
- Fix Error 500s when creating global milestones with Unicode characters (Stan Hu)
v 8.2.3
- Webhook payload has an added, modified and removed properties for each commit
v 8.2.2
- Fix 404 in redirection after removing a project (Stan Hu)
......@@ -13,6 +39,9 @@ v 8.2.2
- Fix Error 500 when viewing user's personal projects from admin page (Stan Hu)
- Fix: Raw private snippets access workflow
- Prevent "413 Request entity too large" errors when pushing large files with LFS
- Fix invalid links within projects dashboard header
- Make current user the first user in assignee dropdown in issues detail page (Stan Hu)
- Fix: duplicate email notifications on issue comments
v 8.2.1
- Forcefully update builds that didn't want to update with state machine
......
......@@ -54,7 +54,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
gem 'net-ldap'
# Git Wiki
gem 'gollum-lib', '~> 4.0.2'
gem 'gollum-lib', '~> 4.1.0'
# Language detection
gem "github-linguist", "~> 4.7.0", require: "linguist"
......@@ -104,7 +104,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'net-ssh', '~> 3.0.1'
gem 'rouge', '~> 1.10.1'
# Diffs
gem 'diffy', '~> 3.0.3'
......@@ -176,6 +176,7 @@ gem "underscore-rails", "~> 1.4.4"
# Sanitize user input
gem "sanitize", '~> 2.0'
gem 'babosa', '~> 1.0.2'
# Protect against bruteforcing
gem "rack-attack", '~> 4.3.0'
......@@ -209,6 +210,7 @@ gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem "gitlab-license", "~> 0.0.4"
......
......@@ -73,6 +73,7 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
bcrypt (3.1.10)
benchmark-ips (2.3.0)
better_errors (1.0.1)
......@@ -316,7 +317,7 @@ GEM
gitlab-license (0.0.4)
gitlab_emoji (0.2.0)
gemojione (~> 2.1)
gitlab_git (7.2.20)
gitlab_git (7.2.21)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -331,11 +332,11 @@ GEM
activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.0)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.0.3)
gollum-lib (4.1.0)
github-markup (~> 1.3.3)
gollum-grit_adapter (~> 1.0)
nokogiri (~> 1.6.4)
rouge (~> 1.10.1)
rouge (~> 1.9)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
gon (6.0.1)
......@@ -842,6 +843,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.2)
attr_encrypted (~> 1.3.4)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
......@@ -889,7 +891,7 @@ DEPENDENCIES
gitlab_git (~> 7.2.20)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.0.2)
gollum-lib (~> 4.1.0)
gon (~> 6.0.1)
grape (~> 0.13.0)
grape-entity (~> 0.4.2)
......@@ -947,6 +949,7 @@ DEPENDENCIES
request_store (~> 1.2.0)
rerun (~> 0.10.0)
responders (~> 2.0)
rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
rubocop (~> 0.28.0)
......
# For DEVELOPMENT only. Production uses Runit in
# https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in
# lib/support/init.d, which call scripts in bin/ .
#
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q mailers -q default
worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
......@@ -106,7 +106,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab
## GitLab release cycle
Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457).
For more information about the release process see the [release documentation](http://doc.gitlab.com/ce/release/).
## Upgrading
......
......@@ -4,6 +4,8 @@
projects_path: "/api/:version/projects.json"
ldap_groups_path: "/api/:version/ldap/:provider/groups.json"
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
......@@ -46,6 +48,35 @@
).done (namespaces) ->
callback(namespaces)
# Return projects list. Filtered by query
projects: (query, callback) ->
url = Api.buildUrl(Api.projects_path)
$.ajax(
url: url
data:
private_token: gon.api_token
search: query
per_page: 20
dataType: "json"
).done (projects) ->
callback(projects)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
url = url.replace(':id', group_id)
$.ajax(
url: url
data:
private_token: gon.api_token
search: query
per_page: 20
dataType: "json"
).done (projects) ->
callback(projects)
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
......
......@@ -137,17 +137,25 @@ $ ->
), 1
# Initialize tooltips
$('body').tooltip({
selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a'
$('body').tooltip(
selector: '.has_tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
if $el.attr('id') == 'js-shortcuts-home'
# Place the logo tooltip on the right when collapsed, bottom when expanded
$el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom'
else
# Otherwise use the data-placement attribute, or 'bottom' if undefined
$el.data('placement') or 'bottom'
})
$el.data('placement') || 'bottom'
)
$('.header-logo .home').tooltip(
placement: (_, el) ->
$el = $(el)
if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom'
container: 'body'
)
$('.page-with-sidebar').tooltip(
selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user'
placement: 'right'
container: 'body'
)
# Form submitter
$('.trigger-submit').on 'change', ->
......
......@@ -89,3 +89,8 @@ class @AwardsHandler
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
scrollToAwards: ->
$('body, html').animate({
scrollTop: $('.awards').offset().top - 80
}, 200)
......@@ -83,7 +83,7 @@ class Dispatcher
when 'projects:project_members:index'
new ProjectMembers()
new UsersSelect()
when 'groups:new', 'groups:edit', 'admin:groups:edit'
when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar()
when 'projects:tree:show'
new TreeView()
......
class @Flash
constructor: (message, type)->
flash = $(".flash-container")
flash.html("")
@flash = $(".flash-container")
@flash.html("")
$('<div/>',
innerDiv = $('<div/>',
class: "flash-#{type}",
text: message
).appendTo(".flash-container")
)
innerDiv.appendTo(".flash-container")
flash.click -> $(@).fadeOut()
flash.show()
@flash.click -> $(@).fadeOut()
@flash.show()
pin: ->
@flash.addClass('flash-pinned flash-raised')
......@@ -29,7 +29,7 @@
$('#filter_issue_search').val($('#issue_search').val())
initSelects: ->
$("select#update_status").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true)
......
......@@ -43,6 +43,7 @@
#
class @MergeRequestTabs
diffsLoaded: false
buildsLoaded: false
commitsLoaded: false
constructor: (@opts = {}) ->
......@@ -54,6 +55,12 @@ class @MergeRequestTabs
bindEvents: ->
$(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown
$(document).on 'click', '.js-show-tab', @showTab
showTab: (event) =>
event.preventDefault()
@activateTab $(event.target).data('action')
tabShown: (event) =>
$target = $(event.target)
......@@ -63,6 +70,8 @@ class @MergeRequestTabs
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
else if action == 'builds'
@loadBuilds($target.attr('href'))
@setCurrentAction(action)
......@@ -101,7 +110,7 @@ class @MergeRequestTabs
action = 'notes' if action == 'show'
# Remove a trailing '/commits' or '/diffs'
new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '')
new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '')
# Append the new action if we're on a tab other than 'notes'
unless action == 'notes'
......@@ -139,6 +148,17 @@ class @MergeRequestTabs
@diffsLoaded = true
@scrollToElement("#diffs")
loadBuilds: (source) ->
return if @buildsLoaded
@_get
url: "#{source}.json"
success: (data) =>
document.getElementById('builds').innerHTML = data.html
$('.js-timeago').timeago()
@buildsLoaded = true
@scrollToElement("#builds")
# Show or hide the loading spinner
#
# status - Boolean, true to show, false to hide
......
......@@ -10,17 +10,20 @@ class @MergeRequestWidget
constructor: (@opts) ->
modal = $('#modal_merge_info').modal(show: false)
mergeInProgress: ->
mergeInProgress: (deleteSourceBranch = false)->
$.ajax
type: 'GET'
url: $('.merge-request').data('url')
success: (data) =>
if data.state == "merged"
location.reload()
urlSuffix = if deleteSourceBranch then '?delete_source=true' else ''
window.location.href = window.location.href + urlSuffix
else if data.merge_error
$('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
else
setTimeout(merge_request_widget.mergeInProgress, 2000)
callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch)
setTimeout(callback, 2000)
dataType: 'json'
rebaseInProgress: ->
......
......@@ -3,7 +3,7 @@ class @NewCommitForm
@newBranch = form.find('.js-new-branch')
@originalBranch = form.find('.js-original-branch')
@createMergeRequest = form.find('.js-create-merge-request')
@createMergeRequestFormGroup = form.find('.js-create-merge-request-form-group')
@createMergeRequestContainer = form.find('.js-create-merge-request-container')
@renderDestination()
@newBranch.keyup @renderDestination
......@@ -12,10 +12,10 @@ class @NewCommitForm
different = @newBranch.val() != @originalBranch.val()
if different
@createMergeRequestFormGroup.show()
@createMergeRequestContainer.show()
@createMergeRequest.prop('checked', true) unless @wasDifferent
else
@createMergeRequestFormGroup.hide()
@createMergeRequestContainer.hide()
@createMergeRequest.prop('checked', false)
@wasDifferent = different
......@@ -111,6 +111,12 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote
###
renderNote: (note) ->
unless note.valid
if note.award
flash = new Flash('You have already used this award emoji!', 'alert')
flash.pin()
return
# render note if it not present in loaded list
# or skip if rendered
if @isNewNote(note) && !note.award
......@@ -122,6 +128,7 @@ class @Notes
if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
awards_handler.scrollToAwards()
###
Check if note does not exists on page
......@@ -362,8 +369,8 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-attachment").remove()
note.find(".note-body > .note-text").show()
note.find(".js-note-attachment-delete").hide()
note.find(".note-edit-form").hide()
note.find(".note-header").show()
note.find(".current-note-edit-form").remove()
###
Called when clicking on the "reply" button for a diff line.
......
class @ProjectSelect
constructor: ->
$('.ajax-project-select').each (i, select) ->
@groupId = $(select).data('group-id')
@includeGroups = $(select).data('include-groups')
placeholder = "Search for project"
placeholder += " or group" if @includeGroups
$(select).select2
placeholder: placeholder
minimumInputLength: 0
query: (query) =>
finalCallback = (projects) ->
data = { results: projects }
query.callback(data)
if @includeGroups
projectsCallback = (projects) ->
groupsCallback = (groups) ->
data = groups.concat(projects)
finalCallback(data)
Api.groups query.term, false, groupsCallback
else
projectsCallback = finalCallback
if @groupId
Api.groupProjects @groupId, query.term, projectsCallback
else
Api.projects query.term, projectsCallback
id: (project) ->
project.web_url
text: (project) ->
project.name_with_namespace || project.name
dropdownCssClass: "ajax-project-dropdown"
......@@ -5,6 +5,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
)
......@@ -2,3 +2,9 @@ class @User
constructor: ->
$('.profile-groups-avatars').tooltip("placement": "top")
new ProjectsList()
$('.hide-project-limit-message').on 'click', (e) ->
path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove()
e.preventDefault()
......@@ -34,17 +34,15 @@ class @UsersSelect
if showNullUser
nullUser = {
name: 'Unassigned',
avatar: null,
username: 'none',
id: 0
}
data.results.unshift(nullUser)
if showAnyUser
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
name: 'Any',
avatar: null,
username: 'none',
name: name,
id: null
}
data.results.unshift(anyUser)
......@@ -52,7 +50,6 @@ class @UsersSelect
if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
emailUser = {
name: "Invite \"#{query.term}\"",
avatar: null,
username: query.term,
id: query.term
}
......@@ -84,10 +81,10 @@ class @UsersSelect
else
avatar = gon.default_avatar_url
"<div class='user-result'>
"<div class='user-result #{'no-username' unless user.username}'>
<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>
<div class='user-name'>#{user.name}</div>
<div class='user-username'>#{user.username}</div>
<div class='user-username'>#{user.username || ""}</div>
</div>"
formatSelection: (user) ->
......
......@@ -116,6 +116,11 @@
position: absolute;
top: 10px;
right: 10px;
&.left {
left: 10px;
right: auto;
}
}
}
......
......@@ -7,8 +7,8 @@
/* Common styles for all types */
.bs-callout {
margin: 20px 0;
padding: 20px;
margin: $gl-padding 0;
padding: $gl-padding;
border-left: 3px solid $border-color;
color: $text-color;
background: $background-color;
......@@ -42,4 +42,3 @@
border-color: #5cA64d;
color: #3c763d;
}
......@@ -342,7 +342,7 @@ table {
}
.well {
margin-bottom: 0;
margin-bottom: $gl-padding;
}
.search_box {
......@@ -350,10 +350,6 @@ table {
text-align: center;
}
.task-status {
margin-left: 10px;
}
#nprogress .spinner {
top: 15px !important;
right: 10px !important;
......@@ -392,9 +388,8 @@ table {
text-align: center;
margin-top: 5px;
margin-bottom: $gl-padding;
height: 56px;
height: auto;
margin-top: -$gl-padding;
padding-top: $gl-padding;
&.no-bottom {
margin-bottom: 0;
......@@ -403,6 +398,18 @@ table {
&.no-top {
margin-top: 0;
}
li a {
display: inline-block;
padding-top: $gl-padding;
padding-bottom: 11px;
margin-bottom: -1px;
}
&.bottom-border {
border-bottom: 1px solid $border-color;
height: 57px;
}
}
.center-middle-menu {
......@@ -450,3 +457,16 @@ table {
.alert, .progress {
margin-bottom: $gl-padding;
}
.new-project-item-select-holder {
display: inline-block;
position: relative;
.new-project-item-select {
position: absolute;
top: 0;
right: 0;
width: 250px !important;
visibility: hidden;
}
}
......@@ -21,7 +21,6 @@
position: relative;
background: $background-color;
border-bottom: 1px solid $border-color;
text-shadow: 0 1px 1px #fff;
margin: 0;
text-align: left;
padding: 10px $gl-padding;
......
......@@ -15,3 +15,13 @@
@extend .alert-danger;
}
}
.flash-pinned {
position: fixed;
top: 80px;
width: 80%;
}
.flash-raised {
z-index: 1000;
}
......@@ -91,9 +91,17 @@ label {
}
.input-group {
.select2-container {
display: table-cell;
width: 200px !important;
}
.input-group-addon {
background-color: #f7f8fa;
}
.input-group-addon:not(:first-child):not(:last-child) {
border-left: 0;
border-right: 0;
}
}
.help-block {
......
......@@ -6,15 +6,17 @@ header {
transition-duration: .3s;
&.navbar-empty {
height: 58px;
background: #FFF;
border-bottom: 1px solid #EEE;
.center-logo {
margin: 8px 0;
margin: 11px 0;
text-align: center;
img {
height: 32px;
#tanuki-logo, img {
width: 36px;
height: 36px;
}
}
}
......
......@@ -6,6 +6,10 @@ html {
body {
background-color: #EAEBEC !important;
&.navless {
background-color: white !important;
}
}
.container {
......@@ -18,8 +22,8 @@ body {
}
.navless-container {
padding-top: $header-height;
margin-top: 30px;
margin-top: $header-height;
padding-top: $gl-padding * 2;
}
.container-limited {
......
......@@ -72,13 +72,6 @@
}
}
ol, ul {
&.styled {
li {
padding: 2px;
}
}
}
/** light list with border-bottom between li **/
ul.bordered-list {
......
......@@ -73,11 +73,8 @@
}
.referenced-users {
padding: 10px 0;
color: #999;
margin-left: 10px;
margin-top: 1px;
margin-right: 130px;
color: #4c4e54;
padding-top: 10px;
}
.md-preview-holder {
......
......@@ -82,9 +82,6 @@
}
.center-top-menu {
height: 45px;
margin-bottom: 30px;
li a {
font-size: 14px;
padding: 19px 10px;
......
......@@ -2,8 +2,10 @@
margin-bottom: $gl-padding;
.panel-heading {
padding: 10px $gl-padding;
padding: 7px $gl-padding;
line-height: 42px !important;
}
.panel-body {
padding: $gl-padding;
......
......@@ -15,6 +15,16 @@
border-left: none;
padding-top: 5px;
}
.select2-chosen {
color: $gl-text-color;
}
&.select2-default {
.select2-chosen {
color: #999;
}
}
}
}
......@@ -23,6 +33,7 @@
border: 1px solid #e7e9ed;
}
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px);
......@@ -48,17 +59,38 @@
color: #313236;
}
.select2-container-multi .select2-choices {
.select2-container-multi {
.select2-choices {
@include border-radius(2px);
border-color: #CCC;
}
border-color: $input-border;
background: white;
padding-left: $gl-padding / 2;
.select2-container-multi .select2-choices .select2-search-field input {
padding: 8px 14px;
.select2-search-field input {
padding: $gl-padding / 2;
font-size: 13px;
line-height: 18px;
height: auto;
font-family: inherit;
font-size: inherit;
}
.select2-search-choice {
margin: 8px 0 0 8px;
background: white;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
.select2-search-choice-close {
top: 5px;
}
&.select2-search-choice-focus {
border-color: $gl-text-color;
}
}
}
}
.select2-drop-active {
......@@ -132,10 +164,16 @@
}
.user-result {
min-height: 24px;
.user-image {
float: left;
}
&.no-username {
.user-name {
line-height: 24px;
}
}
}
......
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
.sidebar-wrapper {
position: fixed;
......@@ -16,7 +17,6 @@
.sidebar-wrapper {
z-index: 99;
background: $background-color;
transition-duration: .3s;
}
.content-wrapper {
......@@ -35,6 +35,83 @@
}
}
.sidebar-wrapper {
.header-logo {
border-bottom: 1px solid transparent;
float: left;
height: $header-height;
width: $sidebar_width;
position: fixed;
z-index: 999;
overflow: hidden;
transition-duration: .3s;
a {
float: left;
height: $header-height;
width: 100%;
padding: 11px 0 11px 22px;
overflow: hidden;
outline: none;
transition-duration: .3s;
img {
width: 36px;
height: 36px;
}
#tanuki-logo, img {
float: left;
}
.gitlab-text-container {
width: 230px;
h3 {
width: 158px;
float: left;
margin: 0;
margin-left: 14px;
font-size: 19px;
line-height: 41px;
font-weight: normal;
}
}
}
&:hover {
background-color: #EEE;
}
}
.sidebar-user {
padding: 9px 22px;
position: fixed;
bottom: 40px;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px;
line-height: 34px;
}
}
}
.tanuki-shape {
transition: all 0.8s;
&:hover {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
}
.nav-sidebar {
margin-top: 14 + $header-height;
margin-bottom: 100px;
......@@ -61,7 +138,7 @@
color: $gray;
display: block;
text-decoration: none;
padding-left: 22px;
padding-left: 23px;
font-weight: normal;
outline: none;
......@@ -85,6 +162,10 @@
padding: 0px 8px;
@include border-radius(6px);
}
&.back-link i {
transition-duration: .3s;
}
}
}
}
......@@ -100,7 +181,6 @@
@mixin expanded-sidebar {
padding-left: $sidebar_width;
transition-duration: .3s;
.sidebar-wrapper {
width: $sidebar_width;
......@@ -114,16 +194,15 @@
&.back-link {
i {
visibility: hidden;
opacity: 0;
}
}
}
}
}
@mixin folded-sidebar {
padding-left: 60px;
transition-duration: .3s;
@mixin collapsed-sidebar {
padding-left: $sidebar_collapsed_width;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
......@@ -132,7 +211,7 @@
width: $sidebar_collapsed_width;
a {
padding-left: 12px;
padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
......@@ -143,19 +222,23 @@
.nav-sidebar {
width: $sidebar_collapsed_width;
li a {
li {
width: auto;
a {
span {
display: none;
}
}
}
}
.collapse-nav a {
width: $sidebar_collapsed_width;
}
.sidebar-user {
padding-left: 12px;
padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
.username {
......@@ -186,11 +269,11 @@
@media (max-width: $screen-md-max) {
.page-sidebar-collapsed {
@include folded-sidebar;
@include collapsed-sidebar;
}
.page-sidebar-expanded {
@include folded-sidebar;
@include collapsed-sidebar;
}
.collapse-nav {
......@@ -200,83 +283,10 @@
@media(min-width: $screen-md-max) {
.page-sidebar-collapsed {
@include folded-sidebar;
@include collapsed-sidebar;
}
.page-sidebar-expanded {
@include expanded-sidebar;
}
}
.sidebar-user {
padding: 9px 22px;
position: fixed;
bottom: 40px;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px;
line-height: 34px;
}
}
.sidebar-wrapper {
.header-logo {
border-bottom: 1px solid transparent;
float: left;
height: $header-height;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
a {
float: left;
height: $header-height;
width: 100%;
padding: 10px 22px;
overflow: hidden;
outline: none;
img {
width: 36px;
height: 36px;
}
#tanuki-logo, img {
float: left;
}
.gitlab-text-container {
width: 230px;
h3 {
width: 158px;
float: left;
margin: 0;
margin-left: 14px;
font-size: 19px;
line-height: 41px;
font-weight: normal;
}
}
}
&:hover {
background-color: #EEE;
}
}
}
.tanuki-shape {
transition: all 0.8s;
&:hover {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
}
......@@ -220,6 +220,7 @@ pre {
.monospace {
font-family: $monospace_font;
font-size: 90%;
}
code {
......
......@@ -67,9 +67,4 @@
color: #3084bb !important;
}
}
.build-top-menu {
margin-top: 0;
margin-bottom: 2px;
}
}
......@@ -90,6 +90,17 @@
}
}
.issuable-show-labels {
a {
margin-right: 5px;
margin-bottom: 5px;
display: inline-block;
.color-label {
padding: 6px 10px;
}
}
}
.cross-project-reference {
text-align: center;
width: 100%;
......@@ -158,6 +169,7 @@
min-width: 214px;
> li {
cursor: pointer;
margin: 5px;
}
}
......
......@@ -56,17 +56,6 @@
}
}
.issue-show-labels {
a {
margin-right: 5px;
margin-bottom: 5px;
display: inline-block;
.color-label {
padding: 6px 10px;
}
}
}
form.edit-issue {
margin: 0;
}
......
/* Login Page */
.login-page {
background-color: white;
.container {
max-width: 960px;
}
......@@ -21,6 +19,7 @@
h1:first-child {
font-weight: normal;
margin-bottom: 30px;
margin-top: 0;
}
img {
......
......@@ -81,6 +81,10 @@
&.ci-error {
color: $gl-danger;
}
a.monospace {
color: inherit;
}
}
.mr-widget-body,
......@@ -136,7 +140,7 @@
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
font-size: 14px;
font-size: 90%;
margin: 0 3px;
}
......@@ -173,27 +177,12 @@
line-height: 1.1;
}
.merge-request-form-info {
padding-top: 15px;
}
// hide mr close link for inline diff comment form
.diff-file .close-mr-link,
.diff-file .reopen-mr-link {
display: none;
}
.merge-request-show-labels {
a {
margin-right: 5px;
margin-bottom: 5px;
display: inline-block;
.color-label {
padding: 6px 10px;
}
}
}
.merge-request-form .select2-container {
width: 250px !important;
}
......
......@@ -5,12 +5,6 @@
}
}
.btn-build-token {
float: left;
padding: 6px 20px;
margin-right: 12px;
}
.profile-avatar-form-option {
hr {
margin: 10px 0;
......
......@@ -5,7 +5,7 @@
font-weight: normal;
}
}
.no-ssh-key-message {
.no-ssh-key-message, .project-limit-message {
background-color: #f28d35;
margin-bottom: 16px;
}
......@@ -84,7 +84,7 @@
@extend .btn-gray;
color: $gray;
cursor: auto;
cursor: default;
i {
color: inherit;
......
.gitlab-ui-dev-kit {
> h2 {
font-size: 27px;
border-bottom: 1px solid #CCC;
color: #666;
margin: 30px 0;
margin: 35px 0 20px;
font-weight: bold;
}
}
......@@ -4,3 +4,8 @@
margin-right: auto;
padding-right: 7px;
}
.wiki-last-edit-by {
font-size: 80%;
font-weight: normal;
}
......@@ -2,8 +2,10 @@ module GlobalMilestones
extend ActiveSupport::Concern
def milestones
epoch = DateTime.parse('1970-01-01')
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
@milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end
......
......@@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_path(title)
group_milestone_path(@group, title.parameterize, title: title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
def projects
......
......@@ -70,6 +70,7 @@ class ProfilesController < Profiles::ApplicationController
:email,
:hide_no_password,
:hide_no_ssh_key,
:hide_project_limit,
:linkedin,
:location,
:name,
......
......@@ -21,14 +21,14 @@ class Projects::ApplicationController < ApplicationController
unless @repository.branch_names.include?(@ref)
redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref),
notice: "This action is not allowed unless you are on top of a branch"
notice: "This action is not allowed unless you are on a branch"
)
end
end
private
def ci_enabled
def builds_enabled
return render_404 unless @project.builds_enabled?
end
......
......@@ -162,12 +162,20 @@ class Projects::BlobController < Projects::ApplicationController
end
def sanitized_new_branch_name
@new_branch ||= sanitize(strip_tags(params[:new_branch]))
sanitize(strip_tags(params[:new_branch]))
end
def editor_variables
@current_branch = @ref
@new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref
@new_branch =
if params[:new_branch].present?
sanitized_new_branch_name
elsif ::Gitlab::GitAccess.new(current_user, @project).can_push_to_branch?(@ref)
@ref
else
@repository.next_patch_branch
end
@file_path =
if action_name.to_s == 'create'
......
......@@ -3,7 +3,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:create, :destroy]
before_action :authorize_push_code!, only: [:new, :create, :destroy]
def index
@sort = params[:sort] || 'name'
......
......@@ -37,7 +37,7 @@ class Projects::CommitController < Projects::ApplicationController
def cancel_builds
ci_commit.builds.running_or_pending.each(&:cancel)
redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha)
redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
def retry_builds
......@@ -47,7 +47,7 @@ class Projects::CommitController < Projects::ApplicationController
end
end
redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha)
redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
def branches
......@@ -75,7 +75,7 @@ class Projects::CommitController < Projects::ApplicationController
@notes_count = commit.notes.count
@builds = ci_commit.builds if ci_commit
@statuses = ci_commit.statuses if ci_commit
end
def authorize_manage_builds!
......
......@@ -5,7 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :ci_enabled, only: :ci
before_action :builds_enabled, only: :ci
def show
respond_to do |format|
......@@ -34,6 +34,26 @@ class Projects::GraphsController < Projects::ApplicationController
@charts[:build_times] = Ci::Charts::BuildTime.new(ci_project)
end
def languages
@languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages
total = @languages.map(&:last).sum
@languages = @languages.map do |language|
name, share = language
color = Digest::SHA256.hexdigest(name)[0...6]
{
value: (share.to_f * 100 / total).round(2),
label: name,
color: "##{color}",
highlight: "##{color}"
}
end
@languages.sort! do |x, y|
y[:value] <=> x[:value]
end
end
private
def fetch_graph
......
......@@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController
def test
if !@project.empty_repo?
status = TestHookService.new.execute(hook, current_user)
status, message = TestHookService.new.execute(hook, current_user)
if status
flash[:notice] = 'Hook successfully executed.'
else
flash[:alert] = 'Hook execution failed. '\
'Ensure hook URL is correct and service is up.'
flash[:alert] = "Hook execution failed: #{message}"
end
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
......
class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :merge, :merge_check,
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
:ci_status, :toggle_subscription, :approve, :ff_merge, :rebase
]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits]
before_action :validates_merge_request, only: [:show, :diffs, :commits]
before_action :define_show_vars, only: [:show, :diffs, :commits]
before_action :ensure_ref_fetched, only: [:show, :commits, :diffs]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request
before_action :authorize_read_merge_request!
......@@ -79,6 +79,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
def builds
@ci_project = @merge_request.source_project.gitlab_ci_project
respond_to do |format|
format.html { render 'show' }
format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
end
end
def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
......@@ -91,23 +100,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit
@first_commit = @merge_request.first_commit
@diffs = @merge_request.compare_diffs
@ci_project = @source_project.gitlab_ci_project
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
set_suggested_approvers
end
def edit
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names
set_suggested_approvers
end
def create
@target_branches ||= []
......@@ -122,6 +130,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
def edit
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names
set_suggested_approvers
end
def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
......@@ -304,6 +320,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request.merge_request_diff
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
......
......@@ -131,7 +131,9 @@ class Projects::NotesController < Projects::ApplicationController
end
def render_note_json(note)
if note.valid?
render json: {
valid: true,
id: note.id,
discussion_id: note.discussion_id,
html: note_to_html(note),
......@@ -141,6 +143,13 @@ class Projects::NotesController < Projects::ApplicationController
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
else
render json: {
valid: false,
award: note.is_award,
errors: note.errors
}
end
end
def authorize_admin_note!
......
......@@ -23,7 +23,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.where(user_id: users)
end
@group_members = @group_members.order('access_level DESC').limit(20)
@group_members = @group_members.order('access_level DESC')
end
@project_member = @project.project_members.new
......
......@@ -10,15 +10,13 @@ class Projects::RawController < Projects::ApplicationController
@blob = @repository.blob_at(@commit.id, @path)
if @blob
type = get_blob_type
headers['X-Content-Type-Options'] = 'nosniff'
send_data(
@blob.data,
type: type,
disposition: 'inline'
)
if @blob.lfs_pointer?
send_lfs_object
else
stream_data
end
else
render_404
end
......@@ -35,4 +33,33 @@ class Projects::RawController < Projects::ApplicationController
'application/octet-stream'
end
end
def stream_data
type = get_blob_type
send_data(
@blob.data,
type: type,
disposition: 'inline'
)
end
def send_lfs_object
lfs_object = find_lfs_object
if lfs_object && lfs_object.project_allowed_access?(@project)
send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment'
else
render_404
end
end
def find_lfs_object
lfs_object = LfsObject.find_by_oid(@blob.lfs_oid)
if lfs_object && lfs_object.file.exists?
lfs_object
else
nil
end
end
end
......@@ -2,7 +2,7 @@ class Projects::TagsController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:create]
before_action :authorize_push_code!, only: [:new, :create]
before_action :authorize_admin_project!, only: [:destroy]
def index
......
class MilestonesFinder
def execute(projects, params)
milestones = Milestone.of_projects(projects)
milestones = milestones.order("due_date ASC")
milestones = milestones.reorder("due_date ASC")
case params[:state]
when 'closed' then milestones.closed
......
......@@ -209,7 +209,7 @@ module ApplicationHelper
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js
element
end
......
......@@ -30,26 +30,24 @@ module BlobHelper
nil
end
if blob && blob.text?
return unless blob && blob.text? && blob_editable?(blob)
text = 'Edit'
after = options[:after] || ''
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
cls = 'btn btn-small'
if allowed_tree_edit?(project, ref)
link_to(text,
namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts),
class: cls
)
else
content_tag :span, text, class: cls + ' disabled'
end + after.html_safe
else
''
) + after.html_safe
end
def blob_editable?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && allowed_tree_edit?(project, ref)
end
def leave_edit_message
......@@ -71,4 +69,16 @@ module BlobHelper
def blob_icon(mode, name)
icon("#{file_type_icon_class('file', mode, name)} fw")
end
def blob_viewable?(blob)
blob && blob.text? && !blob.lfs_pointer?
end
def blob_size(blob)
if blob.lfs_pointer?
blob.lfs_size
else
blob.size
end
end
end
......@@ -8,6 +8,10 @@ module CiStatusHelper
ci_icon_for_status(ci_commit.status)
end
def ci_status_label(ci_commit)
ci_label_for_status(ci_commit.status)
end
def ci_status_color(ci_commit)
case ci_commit.status
when 'success'
......@@ -23,7 +27,15 @@ module CiStatusHelper
def ci_status_with_icon(status)
content_tag :span, class: "ci-status ci-#{status}" do
ci_icon_for_status(status) + '&nbsp;'.html_safe + status
ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
end
end
def ci_label_for_status(status)
if status == 'success'
'passed'
else
status
end
end
......@@ -46,7 +58,7 @@ module CiStatusHelper
def render_ci_status(ci_commit)
link_to ci_status_path(ci_commit),
class: "c#{ci_status_color(ci_commit)}",
title: "Build status: #{ci_commit.status}",
title: "Build status: #{ci_status_label(ci_commit)}",
data: { toggle: 'tooltip', placement: 'left' } do
ci_status_icon(ci_commit)
end
......
......@@ -6,7 +6,7 @@
#
# For example instead of this:
#
# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request)
# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
#
# We can simply use shortcut:
#
......
......@@ -27,16 +27,20 @@ module IconsHelper
end
end
def public_icon
icon('globe fw')
def visibility_level_icon(level, fw: true)
name =
case level
when Gitlab::VisibilityLevel::PRIVATE
'lock'
when Gitlab::VisibilityLevel::INTERNAL
'shield'
else # Gitlab::VisibilityLevel::PUBLIC
'globe'
end
def internal_icon
icon('shield fw')
end
name << " fw" if fw
def private_icon
icon('lock fw')
icon(name)
end
def file_type_icon_class(type, mode, name)
......
......@@ -44,14 +44,17 @@ module IssuesHelper
end
def bulk_update_milestone_options
options_for_select([['None (backlog)', -1]]) +
options_from_collection_for_select(project_active_milestones, 'id',
'title', params[:milestone_id])
milestones = project_active_milestones.to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
end
def milestone_options(object)
options_from_collection_for_select(object.project.milestones.active,
'id', 'title', object.milestone_id)
milestones = object.project.milestones.active.to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
def issue_box_class(item)
......@@ -84,7 +87,11 @@ module IssuesHelper
end
def merge_requests_sentence(merge_requests)
merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
# Sorting based on the `!123` or `group/project!123` reference will sort
# local merge requests first.
merge_requests.map do |merge_request|
merge_request.to_reference(@project)
end.sort.to_sentence(last_word_connector: ', or ')
end
def url_to_emoji(name)
......@@ -96,7 +103,7 @@ module IssuesHelper
def emoji_author_list(notes, current_user)
list = notes.map do |note|
note.author == current_user ? "me" : note.author.username
note.author == current_user ? "me" : note.author.name
end
list.join(", ")
......
......@@ -46,7 +46,11 @@ module MergeRequestsHelper
end
def issues_sentence(issues)
issues.map(&:to_reference).to_sentence
# Sorting based on the `#123` or `group/project#123` reference will sort
# local issues first.
issues.map do |issue|
issue.to_reference(@project)
end.sort.to_sentence
end
def mr_change_branches_path(merge_request)
......@@ -56,8 +60,9 @@ module MergeRequestsHelper
source_project_id: @merge_request.source_project_id,
target_project_id: @merge_request.target_project_id,
source_branch: @merge_request.source_branch,
target_branch: nil
}
target_branch: @merge_request.target_branch,
},
change_branches: true
)
end
......@@ -100,12 +105,14 @@ module MergeRequestsHelper
end
def source_branch_with_namespace(merge_request)
branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
if merge_request.for_fork?
namespace = link_to(merge_request.source_project_namespace,
project_path(merge_request.source_project))
namespace + ":#{merge_request.source_branch}"
namespace + ":" + branch
else
merge_request.source_branch
branch
end
end
......
......@@ -28,7 +28,9 @@ module MilestonesHelper
Milestone.where(project_id: @projects)
end.active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
......
module NamespacesHelper
def namespaces_options(selected = :current_user, scope = :default)
def namespaces_options(selected = :current_user, display_path: false)
groups = current_user.owned_groups + current_user.masters_groups
users = [current_user.namespace]
group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ]
group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ]
users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ]
options = []
options << group_opts
......
......@@ -4,6 +4,14 @@ module NavHelper
end
def nav_sidebar_class
if nav_menu_collapsed?
"sidebar-collapsed"
else
"sidebar-expanded"
end
end
def page_sidebar_class
if nav_menu_collapsed?
"page-sidebar-collapsed"
else
......
......@@ -4,7 +4,8 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any?
@page_title.join(" | ")
# Segments are seperated by middot
@page_title.join(" \u00b7 ")
end
def header_title(title = nil, title_url = nil)
......
......@@ -21,7 +21,7 @@ module ProjectsHelper
end
def link_to_member(project, author, opts = {})
default_opts = { avatar: true, name: true, size: 16, author_class: 'author' }
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
return "(deleted)" unless author
......@@ -39,7 +39,8 @@ module ProjectsHelper
if opts[:name]
link_to(author_html, user_path(author), class: "author_link").html_safe
else
link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe
end
end
......
......@@ -17,13 +17,15 @@ module SelectsHelper
html = {
class: css_class,
'data-placeholder' => placeholder,
'data-null-user' => null_user,
'data-any-user' => any_user,
'data-email-user' => email_user,
'data-first-user' => first_user,
'data-current-user' => current_user,
'data-push-code-to-protected-branches' => push_code_to_protected_branches
data: {
placeholder: placeholder,
null_user: null_user,
any_user: any_user,
email_user: email_user,
first_user: first_user,
current_user: current_user,
"push-code-to-protected-branches" => push_code_to_protected_branches
}
}
unless opts[:scope] == :all
......@@ -57,6 +59,19 @@ module SelectsHelper
select2_tag(id, opts)
end
def project_select_tag(id, opts = {})
opts[:class] ||= ''
opts[:class] << ' ajax-project-select'
unless opts.delete(:scope) == :all
if @group
opts['data-group-id'] = @group.id
end
end
hidden_field_tag(id, opts[:selected], opts)
end
def select2_tag(id, opts = {})
css_class = ''
css_class << 'multiselect ' if opts[:multiple]
......
......@@ -46,12 +46,26 @@ module TreeHelper
File.join(*args)
end
def on_top_of_branch?(project = @project, ref = @ref)
project.repository.branch_names.include?(ref)
end
def allowed_tree_edit?(project = nil, ref = nil)
project ||= @project
ref ||= @ref
return false unless project.repository.branch_names.include?(ref)
return false unless on_top_of_branch?(project, ref)
can?(current_user, :push_code, project)
end
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
def tree_edit_branch(project = @project, ref = @ref)
if allowed_tree_edit?(project, ref)
if can_push_branch?(project, ref)
ref
else
project.repository.next_patch_branch
end
end
end
def tree_breadcrumbs(tree, max_links = 2)
......
......@@ -16,57 +16,37 @@ module VisibilityLevelHelper
# +form_model+ Either a model object (Project, Snippet, etc.) or the name of
# a Project or Snippet class.
def visibility_level_description(level, form_model)
case form_model.is_a?(String) ? form_model : form_model.class.name
when 'PersonalSnippet', 'ProjectSnippet', 'Snippet'
snippet_visibility_level_description(level)
when 'Project'
case form_model
when Project
project_visibility_level_description(level)
when Snippet
snippet_visibility_level_description(level, form_model)
end
end
def project_visibility_level_description(level)
capture_haml do
haml_tag :span do
case level
when Gitlab::VisibilityLevel::PRIVATE
haml_concat "Project access must be granted explicitly for each user."
"Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL
haml_concat "The project can be cloned by"
haml_concat "any logged in user."
"The project can be cloned by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
haml_concat "The project can be cloned"
haml_concat "without any"
haml_concat "authentication."
end
end
"The project can be cloned without any authentication."
end
end
def snippet_visibility_level_description(level)
capture_haml do
haml_tag :span do
def snippet_visibility_level_description(level, snippet = nil)
case level
when Gitlab::VisibilityLevel::PRIVATE
haml_concat "The snippet is visible only for me."
when Gitlab::VisibilityLevel::INTERNAL
haml_concat "The snippet is visible for any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
haml_concat "The snippet can be accessed"
haml_concat "without any"
haml_concat "authentication."
end
if snippet.is_a? ProjectSnippet
"The snippet is visible only to project members."
else
"The snippet is visible only to me."
end
end
end
def visibility_level_icon(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
private_icon
when Gitlab::VisibilityLevel::INTERNAL
internal_icon
"The snippet is visible to any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
public_icon
"The snippet can be accessed without any authentication."
end
end
......
......@@ -31,6 +31,8 @@
#
class ApplicationSetting < ActiveRecord::Base
CACHE_KEY = 'application_setting.last'
serialize :restricted_visibility_levels
serialize :import_sources
serialize :restricted_signup_domains, Array
......@@ -42,12 +44,12 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url,
allow_blank: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" },
url: true,
if: :home_page_url_column_exist
validates :after_sign_out_path,
allow_blank: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
url: true
validates :admin_notification_email,
allow_blank: true,
......@@ -74,21 +76,17 @@ class ApplicationSetting < ActiveRecord::Base
end
after_commit do
Rails.cache.write(cache_key, self)
Rails.cache.write(CACHE_KEY, self)
end
def self.current
Rails.cache.fetch(cache_key) do
Rails.cache.fetch(CACHE_KEY) do
ApplicationSetting.last
end
end
def self.expire
Rails.cache.delete(cache_key)
end
def self.cache_key
'application_setting.last'
Rails.cache.delete(CACHE_KEY)
end
def self.create_from_defaults
......
......@@ -20,8 +20,8 @@ class BroadcastMessage < ActiveRecord::Base
validates :starts_at, presence: true
validates :ends_at, presence: true
validates :color, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true
validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true
validates :color, allow_blank: true, color: true
validates :font, allow_blank: true, color: true
def self.current
where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
......
......@@ -12,17 +12,18 @@
module Ci
class ApplicationSetting < ActiveRecord::Base
extend Ci::Model
CACHE_KEY = 'ci_application_setting.last'
after_commit do
Rails.cache.write(cache_key, self)
Rails.cache.write(CACHE_KEY, self)
end
def self.expire
Rails.cache.delete(cache_key)
Rails.cache.delete(CACHE_KEY)
end
def self.current
Rails.cache.fetch(cache_key) do
Rails.cache.fetch(CACHE_KEY) do
Ci::ApplicationSetting.last
end
end
......@@ -33,9 +34,5 @@ module Ci
add_pusher: Settings.gitlab_ci['add_pusher'],
)
end
def self.cache_key
'ci_application_setting.last'
end
end
end
......@@ -199,7 +199,7 @@ module Ci
end
def ci_yaml_file
gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data
@ci_yaml_file ||= gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data
rescue
nil
end
......
......@@ -20,8 +20,7 @@ module Ci
# HTTParty timeout
default_timeout 10
validates :url, presence: true,
format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
validates :url, presence: true, url: true
def execute(data)
parsed_url = URI.parse(url)
......
......@@ -78,11 +78,23 @@ class Commit
}x
end
def self.link_reference_pattern
super("commit", /(?<commit>\h{6,40})/)
end
def to_reference(from_project = nil)
if cross_project_reference?(from_project)
"#{project.to_reference}@#{id}"
project.to_reference + self.class.reference_prefix + self.id
else
id
self.id
end
end
def reference_link_text(from_project = nil)
if cross_project_reference?(from_project)
project.to_reference + self.class.reference_prefix + self.short_id
else
self.short_id
end
end
......@@ -135,10 +147,10 @@ class Commit
description.present?
end
def hook_attrs
def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace
{
data = {
id: id,
message: safe_message,
timestamp: committed_date.xmlschema,
......@@ -148,6 +160,12 @@ class Commit
email: author_email
}
}
if with_changed_files
data.merge!(repo_changes)
end
data
end
# Discover issues should be closed when this commit is pushed to a project's
......@@ -196,4 +214,22 @@ class Commit
def status
ci_commit.try(:status) || :not_found
end
private
def repo_changes
changes = { added: [], modified: [], removed: [] }
diffs.each do |diff|
if diff.deleted_file
changes[:removed] << diff.old_path
elsif diff.renamed_file || diff.new_file
changes[:added] << diff.new_path
else
changes[:modified] << diff.new_path
end
end
changes
end
end
......@@ -2,36 +2,38 @@
#
# Examples:
#
# range = CommitRange.new('f3f85602...e86e1013')
# range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false
# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013"
#
# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae')
# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true
# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013"
#
# # Assuming `project` is a Project with a repository containing both commits:
# range.project = project
# # Assuming the specified project has a repository containing both commits:
# range.valid_commits? # => true
#
class CommitRange
include ActiveModel::Conversion
include Referable
attr_reader :sha_from, :notation, :sha_to
attr_reader :commit_from, :notation, :commit_to
attr_reader :ref_from, :ref_to
# Optional Project model
attr_accessor :project
# See `exclude_start?`
attr_reader :exclude_start
# The beginning and ending SHAs can be between 6 and 40 hex characters, and
# The beginning and ending refs can be named or SHAs, and
# the range notation can be double- or triple-dot.
PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/
PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
# In text references, the beginning and ending refs can only be SHAs
# between 6 and 40 hex characters.
STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
def self.reference_prefix
'@'
......@@ -43,27 +45,40 @@ class CommitRange
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{PATTERN})
(?<commit_range>#{STRICT_PATTERN})
}x
end
def self.link_reference_pattern
super("compare", /(?<commit_range>#{PATTERN})/)
end
# Initialize a CommitRange
#
# range_string - The String commit range.
# project - An optional Project model.
#
# Raises ArgumentError if `range_string` does not match `PATTERN`.
def initialize(range_string, project = nil)
def initialize(range_string, project)
@project = project
range_string.strip!
unless range_string.match(/\A#{PATTERN}\z/)
unless range_string =~ /\A#{PATTERN}\z/
raise ArgumentError, "invalid CommitRange string format: #{range_string}"
end
@exclude_start = !range_string.include?('...')
@sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2)
@ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2)
@project = project
if project.valid_repo?
@commit_from = project.commit(@ref_from)
@commit_to = project.commit(@ref_to)
end
if valid_commits?
@ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from)
@ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to)
end
end
def inspect
......@@ -71,15 +86,24 @@ class CommitRange
end
def to_s
"#{sha_from[0..7]}#{notation}#{sha_to[0..7]}"
sha_from + notation + sha_to
end
alias_method :id, :to_s
def to_reference(from_project = nil)
# Not using to_s because we want the full SHAs
reference = sha_from + notation + sha_to
if cross_project_reference?(from_project)
project.to_reference + self.class.reference_prefix + self.id
else
self.id
end
end
def reference_link_text(from_project = nil)
reference = ref_from + notation + ref_to
if cross_project_reference?(from_project)
reference = project.to_reference + '@' + reference
reference = project.to_reference + self.class.reference_prefix + reference
end
reference
......@@ -87,46 +111,58 @@ class CommitRange
# Returns a String for use in a link's title attribute
def reference_title
"Commits #{suffixed_sha_from} through #{sha_to}"
"Commits #{sha_start} through #{sha_to}"
end
# Return a Hash of parameters for passing to a URL helper
#
# See `namespace_project_compare_url`
def to_param
{ from: suffixed_sha_from, to: sha_to }
{ from: sha_start, to: sha_to }
end
def exclude_start?
exclude_start
@notation == '..'
end
# Check if both the starting and ending commit IDs exist in a project's
# repository
#
# project - An optional Project to check (default: `project`)
def valid_commits?(project = project)
return nil unless project.present?
return false unless project.valid_repo?
commit_from.present? && commit_to.present?
def valid_commits?
commit_start.present? && commit_end.present?
end
def persisted?
true
end
def commit_from
@commit_from ||= project.repository.commit(suffixed_sha_from)
def sha_from
return nil unless @commit_from
@commit_from.id
end
def sha_to
return nil unless @commit_to
@commit_to.id
end
def commit_to
@commit_to ||= project.repository.commit(sha_to)
def sha_start
return nil unless sha_from
exclude_start? ? sha_from + '^' : sha_from
end
private
def commit_start
return nil unless sha_start
def suffixed_sha_from
sha_from + (exclude_start? ? '^' : '')
if exclude_start?
@commit_start ||= project.commit(sha_start)
else
commit_from
end
end
alias_method :sha_end, :sha_to
alias_method :commit_end, :commit_to
end
......@@ -62,7 +62,12 @@ module Mentionable
return [] if text.blank?
refs = all_references(current_user, text, load_lazy_references: load_lazy_references)
(refs.issues + refs.merge_requests + refs.commits) - [local_reference]
refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
# case for otherwise identical Commit objects.
refs.reject { |ref| ref == local_reference }
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
......
......@@ -21,6 +21,10 @@ module Referable
''
end
def reference_link_text(from_project = nil)
to_reference(from_project)
end
module ClassMethods
# The character that prefixes the actual reference identifier
#
......@@ -44,6 +48,25 @@ module Referable
def reference_pattern
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
def link_reference_pattern(route, pattern)
%r{
(?<url>
#{Regexp.escape(Gitlab.config.gitlab.url)}
\/#{Project.reference_pattern}
\/#{Regexp.escape(route)}
\/#{pattern}
(?<path>
(\/[a-z0-9_=-]+)*
)?
(?<query>
\?[a-z0-9_=-]+
(&[a-z0-9_=-]+)*
)?
(?<anchor>\#[a-z0-9_-]+)?
)
}x
end
end
private
......
......@@ -201,7 +201,7 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
if project.import?
if project.external_import?
"imported"
else
"created"
......
......@@ -16,7 +16,15 @@ class GlobalMilestone
end
def safe_title
@title.parameterize
@title.to_slug.to_s
end
def expired?
if due_date
due_date.past?
else
false
end
end
def projects
......@@ -98,4 +106,25 @@ class GlobalMilestone
def complete?
total_items_count == closed_items_count
end
def due_date
return @due_date if defined?(@due_date)
@due_date =
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
@milestones.first.due_date
else
nil
end
end
def expires_at
if due_date
if due_date.past?
"expired at #{due_date.stamp("Aug 21, 2011")}"
else
"expires at #{due_date.stamp("Aug 21, 2011")}"
end
end
end
end
......@@ -31,13 +31,12 @@ class WebHook < ActiveRecord::Base
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
validates :url, presence: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
validates :url, presence: true, url: true
def execute(data, hook_name)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
WebHook.post(url,
response = WebHook.post(url,
body: data.to_json,
headers: {
"Content-Type" => "application/json",
......@@ -50,7 +49,7 @@ class WebHook < ActiveRecord::Base
username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password),
}
WebHook.post(post_url,
response = WebHook.post(post_url,
body: data.to_json,
headers: {
"Content-Type" => "application/json",
......@@ -59,9 +58,11 @@ class WebHook < ActiveRecord::Base
verify: enable_ssl_verification,
basic_auth: auth)
end
rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
[response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
false
[false, e.to_s]
end
def async_execute(data, hook_name)
......
......@@ -69,6 +69,10 @@ class Issue < ActiveRecord::Base
}x
end
def self.link_reference_pattern
super("issues", /(?<issue>\d+)/)
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
......
......@@ -17,7 +17,7 @@ class Label < ActiveRecord::Base
# Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name)
None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any', '')
Any = LabelStruct.new('Any Label', '')
DEFAULT_COLOR = '#428BCA'
......@@ -27,9 +27,7 @@ class Label < ActiveRecord::Base
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
validates :color,
format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
allow_blank: false
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles
......
# == Schema Information
#
# Table name: lfs_objects
#
# id :integer not null, primary key
# oid :string(255) not null
# size :integer not null
# created_at :datetime
# updated_at :datetime
# file :string(255)
#
class LfsObject < ActiveRecord::Base
has_many :lfs_objects_projects, dependent: :destroy
has_many :projects, through: :lfs_objects_projects
......@@ -5,4 +17,16 @@ class LfsObject < ActiveRecord::Base
validates :oid, presence: true, uniqueness: true
mount_uploader :file, LfsObjectUploader
def storage_project(project)
if project && project.forked?
storage_project(project.forked_from_project)
else
project
end
end
def project_allowed_access?(project)
projects.exists?(storage_project(project).id)
end
end
# == Schema Information
#
# Table name: lfs_objects_projects
#
# id :integer not null, primary key
# lfs_object_id :integer not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
#
class LfsObjectsProject < ActiveRecord::Base
belongs_to :project
belongs_to :lfs_object
......
......@@ -154,6 +154,10 @@ class MergeRequest < ActiveRecord::Base
}x
end
def self.link_reference_pattern
super("merge_requests", /(?<merge_request>\d+)/)
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
......@@ -294,7 +298,7 @@ class MergeRequest < ActiveRecord::Base
work_in_progress: work_in_progress?
}
unless last_commit.nil?
if last_commit
attrs.merge!(last_commit: last_commit.hook_attrs)
end
......@@ -319,7 +323,7 @@ class MergeRequest < ActiveRecord::Base
issues = commits.flat_map { |c| c.closes_issues(current_user) }
issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description))
issues.uniq.sort_by(&:id)
issues.uniq
else
[]
end
......
......@@ -16,9 +16,9 @@
class Milestone < ActiveRecord::Base
# Represents a "No Milestone" state used for filtering Issues and Merge
# Requests that have no milestone assigned.
MilestoneStruct = Struct.new(:title, :name)
None = MilestoneStruct.new('No Milestone', 'No Milestone')
Any = MilestoneStruct.new('Any', '')
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
include InternalId
include Sortable
......
......@@ -23,19 +23,17 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
presence: true, uniqueness: true,
length: { within: 0..255 },
format: { with: Gitlab::Regex.namespace_name_regex,
message: Gitlab::Regex.namespace_name_regex_message }
namespace_name: true,
presence: true,
uniqueness: true
validates :description, length: { within: 0..255 }
validates :path,
uniqueness: { case_sensitive: false },
presence: true,
length: { within: 1..255 },
exclusion: { in: Gitlab::Blacklist.path },
format: { with: Gitlab::Regex.namespace_regex,
message: Gitlab::Regex.namespace_regex_message }
namespace: true,
presence: true,
uniqueness: { case_sensitive: false }
delegate :name, to: :owner, allow_nil: true, prefix: true
......
......@@ -16,6 +16,7 @@
# system :boolean default(FALSE), not null
# st_diff :text
# updated_by_id :integer
# is_award :boolean default(FALSE), not null
#
require 'carrierwave/orm/activerecord'
......@@ -39,9 +40,12 @@ class Note < ActiveRecord::Base
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
validates :line_code, line_code: true, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
......@@ -348,4 +352,31 @@ class Note < ActiveRecord::Base
def editable?
!system?
end
# Checks if note is an award added as a comment
#
# If note is an award, this method sets is_award to true
# and changes content of the note to award name.
#
# Method is executed as a before_validation callback.
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
end
private
def awards_supported?
noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
end
def contains_emoji_only?
note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/
end
def award_emoji_name
note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1]
end
end
......@@ -28,6 +28,7 @@
# import_type :string(255)
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
#
require 'carrierwave/orm/activerecord'
......@@ -159,7 +160,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' },
url: { protocols: %w(ssh git http https) },
if: :external_import?
validates :import_url, presence: true, if: :mirror?
validates :mirror_user, presence: true, if: :mirror?
......
......@@ -23,10 +23,7 @@ class BambooService < CiService
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url,
presence: true,
format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
validates :bamboo_url, presence: true, url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
presence: true,
......
......@@ -21,12 +21,9 @@
class DroneCiService < CiService
prop_accessor :drone_url, :token, :enable_ssl_verification
validates :drone_url,
presence: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated?
validates :token,
presence: true,
if: :activated?
validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
......
......@@ -22,10 +22,8 @@ class ExternalWikiService < Service
include HTTParty
prop_accessor :external_wiki_url
validates :external_wiki_url,
presence: true,
format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
validates :external_wiki_url, presence: true, url: true, if: :activated?
def title
'External Wiki'
......
......@@ -23,16 +23,16 @@ class TeamcityService < CiService
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url,
presence: true,
format: { with: /\A#{URI.regexp}\z/ }, if: :activated?
validates :teamcity_url, presence: true, url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
if: ->(service) { service.password? }, if: :activated?
if: ->(service) { service.password? },
if: :activated?
validates :password,
presence: true,
if: ->(service) { service.username? }, if: :activated?
if: ->(service) { service.username? },
if: :activated?
attr_accessor :response
......
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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