Commit bb79573c authored by Eirik Lygre's avatar Eirik Lygre

Merge branch 'master' into default_clone_protocol_based_on_user_keys

parents 94dc9ef9 9bfd6c44
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased) v 8.3.0 (unreleased)
- Merge when build succeeds (Zeger-Jan van de Weg)
- 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) - 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 - Fix 500 error when update group member permission
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
- Recognize issue/MR/snippet/commit links as references - Recognize issue/MR/snippet/commit links as references
- Add ignore whitespace change option to commit view - Add ignore whitespace change option to commit view
- Fire update hook from GitLab - 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" - Don't show project fork event as "imported"
- Add API endpoint to fetch merge request commits list - Add API endpoint to fetch merge request commits list
- Expose events API with comment information and author info - 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: 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 v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu) - 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 v 8.2.2
- Fix 404 in redirection after removing a project (Stan Hu) - Fix 404 in redirection after removing a project (Stan Hu)
......
...@@ -52,7 +52,7 @@ gem "gitlab_git", '~> 7.2.20' ...@@ -52,7 +52,7 @@ gem "gitlab_git", '~> 7.2.20'
gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki # Git Wiki
gem 'gollum-lib', '~> 4.0.2' gem 'gollum-lib', '~> 4.1.0'
# Language detection # Language detection
gem "github-linguist", "~> 4.7.0", require: "linguist" gem "github-linguist", "~> 4.7.0", require: "linguist"
...@@ -99,7 +99,7 @@ gem 'org-ruby', '~> 0.9.12' ...@@ -99,7 +99,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'net-ssh', '~> 3.0.1' gem 'rouge', '~> 1.10.1'
# Diffs # Diffs
gem 'diffy', '~> 3.0.3' gem 'diffy', '~> 3.0.3'
...@@ -120,8 +120,8 @@ gem 'acts-as-taggable-on', '~> 3.4' ...@@ -120,8 +120,8 @@ gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs # Background jobs
gem 'sinatra', '~> 1.4.4', require: nil gem 'sinatra', '~> 1.4.4', require: nil
gem 'sidekiq', '3.3.0' gem 'sidekiq', '~> 3.5.0'
gem 'sidetiq', '~> 0.6.3' gem 'sidekiq-cron', '~> 0.3.0'
# HTTP requests # HTTP requests
gem "httparty", '~> 0.13.3' gem "httparty", '~> 0.13.3'
...@@ -171,6 +171,7 @@ gem "underscore-rails", "~> 1.4.4" ...@@ -171,6 +171,7 @@ gem "underscore-rails", "~> 1.4.4"
# Sanitize user input # Sanitize user input
gem "sanitize", '~> 2.0' gem "sanitize", '~> 2.0'
gem 'babosa', '~> 1.0.2'
# Protect against bruteforcing # Protect against bruteforcing
gem "rack-attack", '~> 4.3.0' gem "rack-attack", '~> 4.3.0'
...@@ -204,6 +205,7 @@ gem 'raphael-rails', '~> 2.1.2' ...@@ -204,6 +205,7 @@ gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0' gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
group :development do group :development do
gem "foreman" gem "foreman"
......
...@@ -73,6 +73,7 @@ GEM ...@@ -73,6 +73,7 @@ GEM
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
bcrypt (3.1.10) bcrypt (3.1.10)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
better_errors (1.0.1) better_errors (1.0.1)
...@@ -116,8 +117,23 @@ GEM ...@@ -116,8 +117,23 @@ GEM
activemodel (>= 3.2.0) activemodel (>= 3.2.0)
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
json (>= 1.7) json (>= 1.7)
celluloid (0.16.0) celluloid (0.17.2)
timers (~> 4.0.0) celluloid-essentials
celluloid-extras
celluloid-fsm
celluloid-pool
celluloid-supervision
timers (>= 4.1.1)
celluloid-essentials (0.20.5)
timers (>= 4.1.1)
celluloid-extras (0.20.5)
timers (>= 4.1.1)
celluloid-fsm (0.20.5)
timers (>= 4.1.1)
celluloid-pool (0.20.5)
timers (>= 4.1.1)
celluloid-supervision (0.20.5)
timers (>= 4.1.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chunky_png (1.3.5) chunky_png (1.3.5)
cliver (0.3.2) cliver (0.3.2)
...@@ -298,7 +314,7 @@ GEM ...@@ -298,7 +314,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_emoji (0.2.0) gitlab_emoji (0.2.0)
gemojione (~> 2.1) gemojione (~> 2.1)
gitlab_git (7.2.20) gitlab_git (7.2.21)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -313,11 +329,11 @@ GEM ...@@ -313,11 +329,11 @@ GEM
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.0) gollum-grit_adapter (1.0.0)
gitlab-grit (~> 2.7, >= 2.7.1) gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.0.3) gollum-lib (4.1.0)
github-markup (~> 1.3.3) github-markup (~> 1.3.3)
gollum-grit_adapter (~> 1.0) gollum-grit_adapter (~> 1.0)
nokogiri (~> 1.6.4) nokogiri (~> 1.6.4)
rouge (~> 1.10.1) rouge (~> 1.9)
sanitize (~> 2.1.0) sanitize (~> 2.1.0)
stringex (~> 2.5.1) stringex (~> 2.5.1)
gon (6.0.1) gon (6.0.1)
...@@ -369,7 +385,6 @@ GEM ...@@ -369,7 +385,6 @@ GEM
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.7.0.1) httpclient (2.7.0.1)
i18n (0.7.0) i18n (0.7.0)
ice_cube (0.11.1)
ice_nine (0.11.1) ice_nine (0.11.1)
inflecto (0.0.2) inflecto (0.0.2)
ipaddress (0.8.0) ipaddress (0.8.0)
...@@ -640,6 +655,7 @@ GEM ...@@ -640,6 +655,7 @@ GEM
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
rufus-scheduler (3.1.10)
rugged (0.23.3) rugged (0.23.3)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
...@@ -667,16 +683,15 @@ GEM ...@@ -667,16 +683,15 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (3.3.0) sidekiq (3.5.3)
celluloid (>= 0.16.0) celluloid (~> 0.17.2)
connection_pool (>= 2.0.0) connection_pool (~> 2.2, >= 2.2.0)
json json (~> 1.0)
redis (>= 3.0.6) redis (~> 3.2, >= 3.2.1)
redis-namespace (>= 1.3.1) redis-namespace (~> 1.5, >= 1.5.2)
sidetiq (0.6.3) sidekiq-cron (0.3.1)
celluloid (>= 0.14.1) rufus-scheduler (>= 2.0.24)
ice_cube (= 0.11.1) sidekiq (>= 2.17.3)
sidekiq (>= 3.0.0)
simple_oauth (0.1.9) simple_oauth (0.1.9)
simplecov (0.10.0) simplecov (0.10.0)
docile (~> 1.1.0) docile (~> 1.1.0)
...@@ -742,7 +757,7 @@ GEM ...@@ -742,7 +757,7 @@ GEM
thor (0.19.1) thor (0.19.1)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (1.4.1) tilt (1.4.1)
timers (4.0.4) timers (4.1.1)
hitimes hitimes
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tinder (1.10.1) tinder (1.10.1)
...@@ -823,6 +838,7 @@ DEPENDENCIES ...@@ -823,6 +838,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
attr_encrypted (~> 1.3.4) attr_encrypted (~> 1.3.4)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
benchmark-ips benchmark-ips
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
...@@ -868,7 +884,7 @@ DEPENDENCIES ...@@ -868,7 +884,7 @@ DEPENDENCIES
gitlab_git (~> 7.2.20) gitlab_git (~> 7.2.20)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.0.2) gollum-lib (~> 4.1.0)
gon (~> 6.0.1) gon (~> 6.0.1)
grape (~> 0.13.0) grape (~> 0.13.0)
grape-entity (~> 0.4.2) grape-entity (~> 0.4.2)
...@@ -924,6 +940,7 @@ DEPENDENCIES ...@@ -924,6 +940,7 @@ DEPENDENCIES
request_store (~> 1.2.0) request_store (~> 1.2.0)
rerun (~> 0.10.0) rerun (~> 0.10.0)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0) rspec-rails (~> 3.3.0)
rubocop (~> 0.28.0) rubocop (~> 0.28.0)
...@@ -936,8 +953,8 @@ DEPENDENCIES ...@@ -936,8 +953,8 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack sham_rack
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (= 3.3.0) sidekiq (~> 3.5.0)
sidetiq (~> 0.6.3) sidekiq-cron (~> 0.3.0)
simplecov (~> 0.10.0) simplecov (~> 0.10.0)
sinatra (~> 1.4.4) sinatra (~> 1.4.4)
six (~> 0.2.0) six (~> 0.2.0)
......
...@@ -80,7 +80,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab ...@@ -80,7 +80,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab
## GitLab release cycle ## 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 ## Upgrading
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
groups_path: "/api/:version/groups.json" groups_path: "/api/:version/groups.json"
group_path: "/api/:version/groups/:id.json" group_path: "/api/:version/groups/:id.json"
namespaces_path: "/api/:version/namespaces.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) -> group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path) url = Api.buildUrl(Api.group_path)
...@@ -44,6 +46,35 @@ ...@@ -44,6 +46,35 @@
).done (namespaces) -> ).done (namespaces) ->
callback(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) -> buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root? url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version) return url.replace(':version', gon.api_version)
...@@ -88,4 +88,9 @@ class @AwardsHandler ...@@ -88,4 +88,9 @@ class @AwardsHandler
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']") $(".icon[data-emoji='" + emoji + "']")
\ No newline at end of file
scrollToAwards: ->
$('body, html').animate({
scrollTop: $('.awards').offset().top - 80
}, 200)
...@@ -83,7 +83,7 @@ class Dispatcher ...@@ -83,7 +83,7 @@ class Dispatcher
when 'projects:project_members:index' when 'projects:project_members:index'
new ProjectMembers() new ProjectMembers()
new UsersSelect() new UsersSelect()
when 'groups:new', 'groups:edit', 'admin:groups:edit' when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar() new GroupAvatar()
when 'projects:tree:show' when 'projects:tree:show'
new TreeView() new TreeView()
......
class @Flash class @Flash
constructor: (message, type)-> constructor: (message, type)->
flash = $(".flash-container") @flash = $(".flash-container")
flash.html("") @flash.html("")
$('<div/>', innerDiv = $('<div/>',
class: "flash-#{type}", class: "flash-#{type}",
text: message text: message
).appendTo(".flash-container") )
innerDiv.appendTo(".flash-container")
flash.click -> $(@).fadeOut() @flash.click -> $(@).fadeOut()
flash.show() @flash.show()
pin: ->
@flash.addClass('flash-pinned flash-raised')
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
# #
class @MergeRequestTabs class @MergeRequestTabs
diffsLoaded: false diffsLoaded: false
buildsLoaded: false
commitsLoaded: false commitsLoaded: false
constructor: (@opts = {}) -> constructor: (@opts = {}) ->
...@@ -54,6 +55,12 @@ class @MergeRequestTabs ...@@ -54,6 +55,12 @@ class @MergeRequestTabs
bindEvents: -> bindEvents: ->
$(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown $(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) => tabShown: (event) =>
$target = $(event.target) $target = $(event.target)
...@@ -63,6 +70,8 @@ class @MergeRequestTabs ...@@ -63,6 +70,8 @@ class @MergeRequestTabs
@loadCommits($target.attr('href')) @loadCommits($target.attr('href'))
else if action == 'diffs' else if action == 'diffs'
@loadDiff($target.attr('href')) @loadDiff($target.attr('href'))
else if action == 'builds'
@loadBuilds($target.attr('href'))
@setCurrentAction(action) @setCurrentAction(action)
...@@ -101,7 +110,7 @@ class @MergeRequestTabs ...@@ -101,7 +110,7 @@ class @MergeRequestTabs
action = 'notes' if action == 'show' action = 'notes' if action == 'show'
# Remove a trailing '/commits' or '/diffs' # 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' # Append the new action if we're on a tab other than 'notes'
unless action == 'notes' unless action == 'notes'
...@@ -139,6 +148,17 @@ class @MergeRequestTabs ...@@ -139,6 +148,17 @@ class @MergeRequestTabs
@diffsLoaded = true @diffsLoaded = true
@scrollToElement("#diffs") @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 # Show or hide the loading spinner
# #
# status - Boolean, true to show, false to hide # status - Boolean, true to show, false to hide
......
...@@ -3,7 +3,7 @@ class @NewCommitForm ...@@ -3,7 +3,7 @@ class @NewCommitForm
@newBranch = form.find('.js-new-branch') @newBranch = form.find('.js-new-branch')
@originalBranch = form.find('.js-original-branch') @originalBranch = form.find('.js-original-branch')
@createMergeRequest = form.find('.js-create-merge-request') @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() @renderDestination()
@newBranch.keyup @renderDestination @newBranch.keyup @renderDestination
...@@ -12,10 +12,10 @@ class @NewCommitForm ...@@ -12,10 +12,10 @@ class @NewCommitForm
different = @newBranch.val() != @originalBranch.val() different = @newBranch.val() != @originalBranch.val()
if different if different
@createMergeRequestFormGroup.show() @createMergeRequestContainer.show()
@createMergeRequest.prop('checked', true) unless @wasDifferent @createMergeRequest.prop('checked', true) unless @wasDifferent
else else
@createMergeRequestFormGroup.hide() @createMergeRequestContainer.hide()
@createMergeRequest.prop('checked', false) @createMergeRequest.prop('checked', false)
@wasDifferent = different @wasDifferent = different
...@@ -111,6 +111,12 @@ class @Notes ...@@ -111,6 +111,12 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote Note: for rendering inline notes use renderDiscussionNote
### ###
renderNote: (note) -> 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 # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
if @isNewNote(note) && !note.award if @isNewNote(note) && !note.award
...@@ -122,6 +128,7 @@ class @Notes ...@@ -122,6 +128,7 @@ class @Notes
if note.award if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path) awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
awards_handler.scrollToAwards()
### ###
Check if note does not exists on page Check if note does not exists on page
...@@ -362,8 +369,8 @@ class @Notes ...@@ -362,8 +369,8 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-attachment").remove() note.find(".note-attachment").remove()
note.find(".note-body > .note-text").show() note.find(".note-body > .note-text").show()
note.find(".js-note-attachment-delete").hide() note.find(".note-header").show()
note.find(".note-edit-form").hide() note.find(".current-note-edit-form").remove()
### ###
Called when clicking on the "reply" button for a diff line. 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"
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
/* Common styles for all types */ /* Common styles for all types */
.bs-callout { .bs-callout {
margin: 20px 0; margin: $gl-padding 0;
padding: 20px; padding: $gl-padding;
border-left: 3px solid $border-color; border-left: 3px solid $border-color;
color: $text-color; color: $text-color;
background: $background-color; background: $background-color;
...@@ -42,4 +42,3 @@ ...@@ -42,4 +42,3 @@
border-color: #5cA64d; border-color: #5cA64d;
color: #3c763d; color: #3c763d;
} }
...@@ -333,7 +333,7 @@ table { ...@@ -333,7 +333,7 @@ table {
} }
.well { .well {
margin-bottom: 0; margin-bottom: $gl-padding;
} }
.search_box { .search_box {
...@@ -379,9 +379,8 @@ table { ...@@ -379,9 +379,8 @@ table {
text-align: center; text-align: center;
margin-top: 5px; margin-top: 5px;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
height: 56px; height: auto;
margin-top: -$gl-padding; margin-top: -$gl-padding;
padding-top: $gl-padding;
&.no-bottom { &.no-bottom {
margin-bottom: 0; margin-bottom: 0;
...@@ -390,6 +389,18 @@ table { ...@@ -390,6 +389,18 @@ table {
&.no-top { &.no-top {
margin-top: 0; 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 { .center-middle-menu {
...@@ -437,3 +448,16 @@ table { ...@@ -437,3 +448,16 @@ table {
.alert, .progress { .alert, .progress {
margin-bottom: $gl-padding; 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 @@ ...@@ -21,7 +21,6 @@
position: relative; position: relative;
background: $background-color; background: $background-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
text-shadow: 0 1px 1px #fff;
margin: 0; margin: 0;
text-align: left; text-align: left;
padding: 10px $gl-padding; padding: 10px $gl-padding;
......
...@@ -15,3 +15,13 @@ ...@@ -15,3 +15,13 @@
@extend .alert-danger; @extend .alert-danger;
} }
} }
.flash-pinned {
position: fixed;
top: 80px;
width: 80%;
}
.flash-raised {
z-index: 1000;
}
...@@ -72,13 +72,6 @@ ...@@ -72,13 +72,6 @@
} }
} }
ol, ul {
&.styled {
li {
padding: 2px;
}
}
}
/** light list with border-bottom between li **/ /** light list with border-bottom between li **/
ul.bordered-list { ul.bordered-list {
......
...@@ -73,11 +73,8 @@ ...@@ -73,11 +73,8 @@
} }
.referenced-users { .referenced-users {
padding: 10px 0; color: #4c4e54;
color: #999; padding-top: 10px;
margin-left: 10px;
margin-top: 1px;
margin-right: 130px;
} }
.md-preview-holder { .md-preview-holder {
......
...@@ -82,9 +82,6 @@ ...@@ -82,9 +82,6 @@
} }
.center-top-menu { .center-top-menu {
height: 45px;
margin-bottom: 30px;
li a { li a {
font-size: 14px; font-size: 14px;
padding: 19px 10px; padding: 19px 10px;
......
.panel { .panel {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
.panel-heading { .panel-heading {
padding: 10px $gl-padding; padding: 7px $gl-padding;
line-height: 42px !important;
} }
.panel-body { .panel-body {
padding: $gl-padding; padding: $gl-padding;
......
...@@ -220,6 +220,7 @@ pre { ...@@ -220,6 +220,7 @@ pre {
.monospace { .monospace {
font-family: $monospace_font; font-family: $monospace_font;
font-size: 90%;
} }
code { code {
......
...@@ -67,9 +67,4 @@ ...@@ -67,9 +67,4 @@
color: #3084bb !important; color: #3084bb !important;
} }
} }
.build-top-menu {
margin-top: 0;
margin-bottom: 2px;
}
} }
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
.accept-merge-holder { .accept-merge-holder {
.accept-action { .accept-action {
display: inline-block; display: inline-block;
float: left;
.accept_merge_request { .accept_merge_request {
&.ci-pending, &.ci-pending,
...@@ -36,14 +37,15 @@ ...@@ -36,14 +37,15 @@
.accept-control { .accept-control {
display: inline-block; display: inline-block;
float: left;
margin: 0; margin: 0;
margin-left: 20px; margin-left: 20px;
padding: 5px; padding: 5px;
padding-top: 12px;
line-height: 20px; line-height: 20px;
&.right { &.right {
float: right; float: right;
padding-top: 12px;
a { a {
color: $gl-gray; color: $gl-gray;
} }
...@@ -81,6 +83,10 @@ ...@@ -81,6 +83,10 @@
&.ci-error { &.ci-error {
color: $gl-danger; color: $gl-danger;
} }
a.monospace {
color: inherit;
}
} }
.mr-widget-body, .mr-widget-body,
...@@ -136,7 +142,7 @@ ...@@ -136,7 +142,7 @@
font-family: $monospace_font; font-family: $monospace_font;
font-weight: bold; font-weight: bold;
overflow: hidden; overflow: hidden;
font-size: 14px; font-size: 90%;
margin: 0 3px; margin: 0 3px;
} }
......
...@@ -5,12 +5,6 @@ ...@@ -5,12 +5,6 @@
} }
} }
.btn-build-token {
float: left;
padding: 6px 20px;
margin-right: 12px;
}
.profile-avatar-form-option { .profile-avatar-form-option {
hr { hr {
margin: 10px 0; margin: 10px 0;
......
.gitlab-ui-dev-kit { .gitlab-ui-dev-kit {
> h2 { > h2 {
font-size: 27px; margin: 35px 0 20px;
border-bottom: 1px solid #CCC;
color: #666;
margin: 30px 0;
font-weight: bold; font-weight: bold;
} }
} }
...@@ -2,8 +2,10 @@ module GlobalMilestones ...@@ -2,8 +2,10 @@ module GlobalMilestones
extend ActiveSupport::Concern extend ActiveSupport::Concern
def milestones def milestones
epoch = DateTime.parse('1970-01-01')
@milestones = MilestonesFinder.new.execute(@projects, params) @milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones) @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) @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end end
......
...@@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def milestone_path(title) def milestone_path(title)
group_milestone_path(@group, title.parameterize, title: title) group_milestone_path(@group, title.to_slug.to_s, title: title)
end end
def projects def projects
......
...@@ -21,14 +21,14 @@ class Projects::ApplicationController < ApplicationController ...@@ -21,14 +21,14 @@ class Projects::ApplicationController < ApplicationController
unless @repository.branch_names.include?(@ref) unless @repository.branch_names.include?(@ref)
redirect_to( redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref), 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
end end
private private
def ci_enabled def builds_enabled
return render_404 unless @project.builds_enabled? return render_404 unless @project.builds_enabled?
end end
......
...@@ -162,12 +162,20 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -162,12 +162,20 @@ class Projects::BlobController < Projects::ApplicationController
end end
def sanitized_new_branch_name def sanitized_new_branch_name
@new_branch ||= sanitize(strip_tags(params[:new_branch])) sanitize(strip_tags(params[:new_branch]))
end end
def editor_variables def editor_variables
@current_branch = @ref @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 = @file_path =
if action_name.to_s == 'create' if action_name.to_s == 'create'
......
...@@ -37,7 +37,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -37,7 +37,7 @@ class Projects::CommitController < Projects::ApplicationController
def cancel_builds def cancel_builds
ci_commit.builds.running_or_pending.each(&:cancel) 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 end
def retry_builds def retry_builds
...@@ -47,7 +47,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -47,7 +47,7 @@ class Projects::CommitController < Projects::ApplicationController
end end
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 end
def branches def branches
...@@ -74,8 +74,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -74,8 +74,8 @@ class Projects::CommitController < Projects::ApplicationController
end end
@notes_count = commit.notes.count @notes_count = commit.notes.count
@builds = ci_commit.builds if ci_commit @statuses = ci_commit.statuses if ci_commit
end end
def authorize_manage_builds! def authorize_manage_builds!
......
...@@ -5,7 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController ...@@ -5,7 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars before_action :assign_ref_vars
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :ci_enabled, only: :ci before_action :builds_enabled, only: :ci
def show def show
respond_to do |format| respond_to do |format|
...@@ -34,6 +34,26 @@ class Projects::GraphsController < Projects::ApplicationController ...@@ -34,6 +34,26 @@ class Projects::GraphsController < Projects::ApplicationController
@charts[:build_times] = Ci::Charts::BuildTime.new(ci_project) @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project)
end 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 private
def fetch_graph def fetch_graph
......
...@@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController
def test def test
if !@project.empty_repo? if !@project.empty_repo?
status = TestHookService.new.execute(hook, current_user) status, message = TestHookService.new.execute(hook, current_user)
if status if status
flash[:notice] = 'Hook successfully executed.' flash[:notice] = 'Hook successfully executed.'
else else
flash[:alert] = 'Hook execution failed. '\ flash[:alert] = "Hook execution failed: #{message}"
'Ensure hook URL is correct and service is up.'
end end
else else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.' flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
......
class Projects::MergeRequestsController < Projects::ApplicationController class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ 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 :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
] ]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
before_action :define_show_vars, only: [:show, :diffs, :commits] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
before_action :ensure_ref_fetched, only: [:show, :commits, :diffs] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -79,6 +80,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -79,6 +80,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
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 def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
...@@ -91,20 +101,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -91,20 +101,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project @target_project = merge_request.target_project
@source_project = merge_request.source_project @source_project = merge_request.source_project
@commits = @merge_request.compare_commits @commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit @commit = @merge_request.last_commit
@first_commit = @merge_request.first_commit @first_commit = @merge_request.first_commit
@diffs = @merge_request.compare_diffs @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)). @note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
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
end
def create def create
@target_branches ||= [] @target_branches ||= []
@merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
...@@ -118,6 +127,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -118,6 +127,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
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
end
def update def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
...@@ -150,15 +165,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -150,15 +165,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render partial: "projects/merge_requests/widget/show.html.haml", layout: false render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end end
def cancel_merge_when_build_succeeds
return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request)
end
def merge def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user) return access_denied! unless @merge_request.can_be_merged_by?(current_user)
if @merge_request.mergeable? unless @merge_request.mergeable?
@merge_request.update(merge_error: nil) @status = :failed
MergeWorker.perform_async(@merge_request.id, current_user.id, params) return
@status = true end
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds] && @merge_request.ci_commit && @merge_request.ci_commit.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
else else
@status = false MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
end end
end end
...@@ -264,12 +293,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -264,12 +293,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request.merge_request_diff @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? if @merge_request.locked_long_ago?
@merge_request.unlock_mr @merge_request.unlock_mr
@merge_request.close @merge_request.close
end end
end end
def define_widget_vars
@ci_commit = @merge_request.ci_commit
end
def invalid_mr def invalid_mr
# Render special view for MR with removed source or target branch # Render special view for MR with removed source or target branch
render 'invalid' render 'invalid'
...@@ -283,6 +319,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -283,6 +319,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def merge_params
params.permit(:should_remove_source_branch, :commit_message)
end
# Make sure merge requests created before 8.0 # Make sure merge requests created before 8.0
# have head file in refs/merge-requests/ # have head file in refs/merge-requests/
def ensure_ref_fetched def ensure_ref_fetched
......
...@@ -131,16 +131,25 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -131,16 +131,25 @@ class Projects::NotesController < Projects::ApplicationController
end end
def render_note_json(note) def render_note_json(note)
render json: { if note.valid?
id: note.id, render json: {
discussion_id: note.discussion_id, valid: true,
html: note_to_html(note), id: note.id,
award: note.is_award, discussion_id: note.discussion_id,
emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "", html: note_to_html(note),
note: note.note, award: note.is_award,
discussion_html: note_to_discussion_html(note), emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
discussion_with_diff_html: note_to_discussion_with_diff_html(note) note: note.note,
} 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 end
def authorize_admin_note! def authorize_admin_note!
......
...@@ -23,7 +23,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.where(user_id: users) @group_members = @group_members.where(user_id: users)
end end
@group_members = @group_members.order('access_level DESC').limit(20) @group_members = @group_members.order('access_level DESC')
end end
@project_member = @project.project_members.new @project_member = @project.project_members.new
......
...@@ -10,15 +10,13 @@ class Projects::RawController < Projects::ApplicationController ...@@ -10,15 +10,13 @@ class Projects::RawController < Projects::ApplicationController
@blob = @repository.blob_at(@commit.id, @path) @blob = @repository.blob_at(@commit.id, @path)
if @blob if @blob
type = get_blob_type
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
send_data( if @blob.lfs_pointer?
@blob.data, send_lfs_object
type: type, else
disposition: 'inline' stream_data
) end
else else
render_404 render_404
end end
...@@ -35,4 +33,33 @@ class Projects::RawController < Projects::ApplicationController ...@@ -35,4 +33,33 @@ class Projects::RawController < Projects::ApplicationController
'application/octet-stream' 'application/octet-stream'
end end
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 end
class MilestonesFinder class MilestonesFinder
def execute(projects, params) def execute(projects, params)
milestones = Milestone.of_projects(projects) milestones = Milestone.of_projects(projects)
milestones = milestones.order("due_date ASC") milestones = milestones.reorder("due_date ASC")
case params[:state] case params[:state]
when 'closed' then milestones.closed when 'closed' then milestones.closed
......
...@@ -209,7 +209,7 @@ module ApplicationHelper ...@@ -209,7 +209,7 @@ module ApplicationHelper
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement, container: 'body' } 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 element
end end
......
...@@ -30,26 +30,24 @@ module BlobHelper ...@@ -30,26 +30,24 @@ module BlobHelper
nil nil
end end
if blob && blob.text? return unless blob && blob.text? && blob_editable?(blob)
text = 'Edit'
after = options[:after] || '' text = 'Edit'
from_mr = options[:from_merge_request_id] after = options[:after] || ''
link_opts = {} from_mr = options[:from_merge_request_id]
link_opts[:from_merge_request_id] = from_mr if from_mr link_opts = {}
cls = 'btn btn-small' link_opts[:from_merge_request_id] = from_mr if from_mr
if allowed_tree_edit?(project, ref) cls = 'btn btn-small'
link_to(text, link_to(text,
namespace_project_edit_blob_path(project.namespace, project, namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path), tree_join(ref, path),
link_opts), link_opts),
class: cls class: cls
) ) + after.html_safe
else end
content_tag :span, text, class: cls + ' disabled'
end + after.html_safe def blob_editable?(blob, project = @project, ref = @ref)
else !blob.lfs_pointer? && allowed_tree_edit?(project, ref)
''
end
end end
def leave_edit_message def leave_edit_message
...@@ -71,4 +69,16 @@ module BlobHelper ...@@ -71,4 +69,16 @@ module BlobHelper
def blob_icon(mode, name) def blob_icon(mode, name)
icon("#{file_type_icon_class('file', mode, name)} fw") icon("#{file_type_icon_class('file', mode, name)} fw")
end 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 end
...@@ -11,7 +11,7 @@ module BranchesHelper ...@@ -11,7 +11,7 @@ module BranchesHelper
def can_push_branch?(project, branch_name) def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name) return false unless project.repository.branch_names.include?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end end
end end
...@@ -8,6 +8,10 @@ module CiStatusHelper ...@@ -8,6 +8,10 @@ module CiStatusHelper
ci_icon_for_status(ci_commit.status) ci_icon_for_status(ci_commit.status)
end end
def ci_status_label(ci_commit)
ci_label_for_status(ci_commit.status)
end
def ci_status_color(ci_commit) def ci_status_color(ci_commit)
case ci_commit.status case ci_commit.status
when 'success' when 'success'
...@@ -23,7 +27,15 @@ module CiStatusHelper ...@@ -23,7 +27,15 @@ module CiStatusHelper
def ci_status_with_icon(status) def ci_status_with_icon(status)
content_tag :span, class: "ci-status ci-#{status}" do 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
end end
...@@ -46,7 +58,7 @@ module CiStatusHelper ...@@ -46,7 +58,7 @@ module CiStatusHelper
def render_ci_status(ci_commit) def render_ci_status(ci_commit)
link_to ci_status_path(ci_commit), link_to ci_status_path(ci_commit),
class: "c#{ci_status_color(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 data: { toggle: 'tooltip', placement: 'left' } do
ci_status_icon(ci_commit) ci_status_icon(ci_commit)
end end
......
...@@ -28,7 +28,9 @@ module MilestonesHelper ...@@ -28,7 +28,9 @@ module MilestonesHelper
Milestone.where(project_id: @projects) Milestone.where(project_id: @projects)
end.active end.active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones) 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::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
......
...@@ -4,7 +4,8 @@ module PageLayoutHelper ...@@ -4,7 +4,8 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any? @page_title.push(*titles.compact) if titles.any?
@page_title.join(" | ") # Segments are seperated by middot
@page_title.join(" \u00b7 ")
end end
def header_title(title = nil, title_url = nil) def header_title(title = nil, title_url = nil)
......
...@@ -48,6 +48,19 @@ module SelectsHelper ...@@ -48,6 +48,19 @@ module SelectsHelper
select2_tag(id, opts) select2_tag(id, opts)
end 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 = {}) def select2_tag(id, opts = {})
css_class = '' css_class = ''
css_class << 'multiselect ' if opts[:multiple] css_class << 'multiselect ' if opts[:multiple]
......
...@@ -46,12 +46,26 @@ module TreeHelper ...@@ -46,12 +46,26 @@ module TreeHelper
File.join(*args) File.join(*args)
end 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) def allowed_tree_edit?(project = nil, ref = nil)
project ||= @project project ||= @project
ref ||= @ref 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 end
def tree_breadcrumbs(tree, max_links = 2) def tree_breadcrumbs(tree, max_links = 2)
......
...@@ -12,22 +12,22 @@ module VisibilityLevelHelper ...@@ -12,22 +12,22 @@ module VisibilityLevelHelper
# Return the description for the +level+ argument. # Return the description for the +level+ argument.
# #
# +level+ One of the Gitlab::VisibilityLevel constants # +level+ One of the Gitlab::VisibilityLevel constants
# +form_model+ Either a model object (Project, Snippet, etc.) or the name of # +form_model+ Either a model object (Project, Snippet, etc.) or the name of
# a Project or Snippet class. # a Project or Snippet class.
def visibility_level_description(level, form_model) def visibility_level_description(level, form_model)
case form_model.is_a?(String) ? form_model : form_model.class.name case form_model
when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' when Project
snippet_visibility_level_description(level)
when 'Project'
project_visibility_level_description(level) project_visibility_level_description(level)
when Snippet
snippet_visibility_level_description(level, form_model)
end end
end end
def project_visibility_level_description(level) def project_visibility_level_description(level)
case level case level
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
"Project access must be granted explicitly for each user." "Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL when Gitlab::VisibilityLevel::INTERNAL
"The project can be cloned by any logged in user." "The project can be cloned by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC when Gitlab::VisibilityLevel::PUBLIC
...@@ -35,12 +35,16 @@ module VisibilityLevelHelper ...@@ -35,12 +35,16 @@ module VisibilityLevelHelper
end end
end end
def snippet_visibility_level_description(level) def snippet_visibility_level_description(level, snippet = nil)
case level case level
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
"The snippet is visible only for me." if snippet.is_a? ProjectSnippet
"The snippet is visible only to project members."
else
"The snippet is visible only to me."
end
when Gitlab::VisibilityLevel::INTERNAL when Gitlab::VisibilityLevel::INTERNAL
"The snippet is visible for any logged in user." "The snippet is visible to any logged in user."
when Gitlab::VisibilityLevel::PUBLIC when Gitlab::VisibilityLevel::PUBLIC
"The snippet can be accessed without any authentication." "The snippet can be accessed without any authentication."
end end
......
...@@ -43,12 +43,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -43,12 +43,12 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url, validates :home_page_url,
allow_blank: true, 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 if: :home_page_url_column_exist
validates :after_sign_out_path, validates :after_sign_out_path,
allow_blank: true, allow_blank: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } url: true
validates :admin_notification_email, validates :admin_notification_email,
allow_blank: true, allow_blank: true,
......
...@@ -16,12 +16,12 @@ ...@@ -16,12 +16,12 @@
class BroadcastMessage < ActiveRecord::Base class BroadcastMessage < ActiveRecord::Base
include Sortable include Sortable
validates :message, presence: true validates :message, presence: true
validates :starts_at, presence: true validates :starts_at, presence: true
validates :ends_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 :color, allow_blank: true, color: true
validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true validates :font, allow_blank: true, color: true
def self.current def self.current
where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
......
...@@ -165,6 +165,14 @@ module Ci ...@@ -165,6 +165,14 @@ module Ci
status == 'canceled' status == 'canceled'
end end
def active?
running? || pending?
end
def complete?
canceled? || success? || failed?
end
def duration def duration
duration_array = latest_statuses.map(&:duration).compact duration_array = latest_statuses.map(&:duration).compact
duration_array.reduce(:+).to_i duration_array.reduce(:+).to_i
...@@ -199,7 +207,7 @@ module Ci ...@@ -199,7 +207,7 @@ module Ci
end end
def ci_yaml_file 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 rescue
nil nil
end end
......
...@@ -20,8 +20,7 @@ module Ci ...@@ -20,8 +20,7 @@ module Ci
# HTTParty timeout # HTTParty timeout
default_timeout 10 default_timeout 10
validates :url, presence: true, validates :url, presence: true, url: true
format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
def execute(data) def execute(data)
parsed_url = URI.parse(url) parsed_url = URI.parse(url)
......
...@@ -147,10 +147,10 @@ class Commit ...@@ -147,10 +147,10 @@ class Commit
description.present? description.present?
end end
def hook_attrs def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace path_with_namespace = project.path_with_namespace
{ data = {
id: id, id: id,
message: safe_message, message: safe_message,
timestamp: committed_date.xmlschema, timestamp: committed_date.xmlschema,
...@@ -160,6 +160,12 @@ class Commit ...@@ -160,6 +160,12 @@ class Commit
email: author_email email: author_email
} }
} }
if with_changed_files
data.merge!(repo_changes)
end
data
end end
# Discover issues should be closed when this commit is pushed to a project's # Discover issues should be closed when this commit is pushed to a project's
...@@ -208,4 +214,22 @@ class Commit ...@@ -208,4 +214,22 @@ class Commit
def status def status
ci_commit.try(:status) || :not_found ci_commit.try(:status) || :not_found
end 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 end
# == Schema Information # == Schema Information
# #
# Table name: ci_builds # project_id integer
# # status string
# id :integer not null, primary key # finished_at datetime
# project_id :integer # trace text
# status :string(255) # created_at datetime
# finished_at :datetime # updated_at datetime
# trace :text # started_at datetime
# created_at :datetime # runner_id integer
# updated_at :datetime # coverage float
# started_at :datetime # commit_id integer
# runner_id :integer # commands text
# coverage :float # job_id integer
# commit_id :integer # name string
# commands :text # deploy boolean default: false
# job_id :integer # options text
# name :string(255) # allow_failure boolean default: false, null: false
# deploy :boolean default(FALSE) # stage string
# options :text # trigger_request_id integer
# allow_failure :boolean default(FALSE), not null # stage_idx integer
# stage :string(255) # tag boolean
# trigger_request_id :integer # ref string
# stage_idx :integer # user_id integer
# tag :boolean # type string
# ref :string(255) # target_url string
# user_id :integer # description string
# type :string(255)
# target_url :string(255)
# description :string(255)
# artifacts_file :text
# #
class CommitStatus < ActiveRecord::Base class CommitStatus < ActiveRecord::Base
...@@ -79,6 +75,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -79,6 +75,10 @@ class CommitStatus < ActiveRecord::Base
build.update_attributes finished_at: Time.now build.update_attributes finished_at: Time.now
end end
after_transition [:pending, :running] => :success do |build, transition|
MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.gl_project, nil).trigger(build)
end
state :pending, value: 'pending' state :pending, value: 'pending'
state :running, value: 'running' state :running, value: 'running'
state :failed, value: 'failed' state :failed, value: 'failed'
......
...@@ -16,7 +16,15 @@ class GlobalMilestone ...@@ -16,7 +16,15 @@ class GlobalMilestone
end end
def safe_title def safe_title
@title.parameterize @title.to_slug.to_s
end
def expired?
if due_date
due_date.past?
else
false
end
end end
def projects def projects
...@@ -98,4 +106,25 @@ class GlobalMilestone ...@@ -98,4 +106,25 @@ class GlobalMilestone
def complete? def complete?
total_items_count == closed_items_count total_items_count == closed_items_count
end 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 end
...@@ -31,37 +31,38 @@ class WebHook < ActiveRecord::Base ...@@ -31,37 +31,38 @@ class WebHook < ActiveRecord::Base
# HTTParty timeout # HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout default_timeout Gitlab.config.gitlab.webhook_timeout
validates :url, presence: true, validates :url, presence: true, url: true
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
def execute(data, hook_name) def execute(data, hook_name)
parsed_url = URI.parse(url) parsed_url = URI.parse(url)
if parsed_url.userinfo.blank? if parsed_url.userinfo.blank?
WebHook.post(url, response = WebHook.post(url,
body: data.to_json, body: data.to_json,
headers: { headers: {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize "X-Gitlab-Event" => hook_name.singularize.titleize
}, },
verify: enable_ssl_verification) verify: enable_ssl_verification)
else else
post_url = url.gsub("#{parsed_url.userinfo}@", "") post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = { auth = {
username: URI.decode(parsed_url.user), username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password), password: URI.decode(parsed_url.password),
} }
WebHook.post(post_url, response = WebHook.post(post_url,
body: data.to_json, body: data.to_json,
headers: { headers: {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize "X-Gitlab-Event" => hook_name.singularize.titleize
}, },
verify: enable_ssl_verification, verify: enable_ssl_verification,
basic_auth: auth) basic_auth: auth)
end 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}") logger.error("WebHook Error => #{e}")
false [false, e.to_s]
end end
def async_execute(data, hook_name) def async_execute(data, hook_name)
......
...@@ -27,9 +27,7 @@ class Label < ActiveRecord::Base ...@@ -27,9 +27,7 @@ class Label < ActiveRecord::Base
has_many :label_links, dependent: :destroy has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
validates :color, validates :color, color: true, allow_blank: false
format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? } validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles # 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 class LfsObject < ActiveRecord::Base
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :projects, through: :lfs_objects_projects has_many :projects, through: :lfs_objects_projects
...@@ -5,4 +17,16 @@ class LfsObject < ActiveRecord::Base ...@@ -5,4 +17,16 @@ class LfsObject < ActiveRecord::Base
validates :oid, presence: true, uniqueness: true validates :oid, presence: true, uniqueness: true
mount_uploader :file, LfsObjectUploader 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 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 class LfsObjectsProject < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :lfs_object belongs_to :lfs_object
......
...@@ -2,25 +2,28 @@ ...@@ -2,25 +2,28 @@
# #
# Table name: merge_requests # Table name: merge_requests
# #
# id :integer not null, primary key # id :integer not null, primary key
# target_branch :string(255) not null # target_branch :string(255) not null
# source_branch :string(255) not null # source_branch :string(255) not null
# source_project_id :integer not null # source_project_id :integer not null
# author_id :integer # author_id :integer
# assignee_id :integer # assignee_id :integer
# title :string(255) # title :string(255)
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# milestone_id :integer # milestone_id :integer
# state :string(255) # state :string(255)
# merge_status :string(255) # merge_status :string(255)
# target_project_id :integer not null # target_project_id :integer not null
# iid :integer # iid :integer
# description :text # description :text
# position :integer default(0) # position :integer default(0)
# locked_at :datetime # locked_at :datetime
# updated_by_id :integer # updated_by_id :integer
# merge_error :string(255) # merge_error :string(255)
# merge_params :text (serialized to hash)
# merge_when_build_succeeds :boolean default(false), not null
# merge_user_id :integer
# #
require Rails.root.join("app/models/commit") require Rails.root.join("app/models/commit")
...@@ -35,9 +38,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -35,9 +38,12 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
belongs_to :merge_user, class_name: "User"
has_one :merge_request_diff, dependent: :destroy has_one :merge_request_diff, dependent: :destroy
serialize :merge_params, Hash
after_create :create_merge_request_diff after_create :create_merge_request_diff
after_update :update_merge_request_diff after_update :update_merge_request_diff
...@@ -121,6 +127,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -121,6 +127,7 @@ class MergeRequest < ActiveRecord::Base
validates :source_branch, presence: true validates :source_branch, presence: true
validates :target_project, presence: true validates :target_project, presence: true
validates :target_branch, presence: true validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches validate :validate_branches
validate :validate_fork validate :validate_fork
...@@ -258,6 +265,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -258,6 +265,16 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def can_cancel_merge_when_build_succeeds?(current_user)
can_be_merged_by?(current_user) || self.author == current_user
end
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.abilities.allowed?(current_user, :push_code, source_project)
end
def mr_and_commit_notes def mr_and_commit_notes
# Fetch comments only from last 100 commits # Fetch comments only from last 100 commits
commits_for_notes_limit = 100 commits_for_notes_limit = 100
...@@ -295,7 +312,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -295,7 +312,7 @@ class MergeRequest < ActiveRecord::Base
work_in_progress: work_in_progress? work_in_progress: work_in_progress?
} }
unless last_commit.nil? if last_commit
attrs.merge!(last_commit: last_commit.hook_attrs) attrs.merge!(last_commit: last_commit.hook_attrs)
end end
...@@ -393,6 +410,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -393,6 +410,16 @@ class MergeRequest < ActiveRecord::Base
message message
end end
def reset_merge_when_build_succeeds
return unless merge_when_build_succeeds?
self.merge_when_build_succeeds = false
self.merge_user = nil
self.merge_params = nil
self.save
end
# Return array of possible target branches # Return array of possible target branches
# depends on target project of MR # depends on target project of MR
def target_branches def target_branches
...@@ -480,8 +507,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -480,8 +507,6 @@ class MergeRequest < ActiveRecord::Base
end end
def ci_commit def ci_commit
if last_commit and source_project @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
source_project.ci_commit(last_commit.id)
end
end end
end end
...@@ -23,19 +23,17 @@ class Namespace < ActiveRecord::Base ...@@ -23,19 +23,17 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
presence: true, uniqueness: true,
length: { within: 0..255 }, length: { within: 0..255 },
format: { with: Gitlab::Regex.namespace_name_regex, namespace_name: true,
message: Gitlab::Regex.namespace_name_regex_message } presence: true,
uniqueness: true
validates :description, length: { within: 0..255 } validates :description, length: { within: 0..255 }
validates :path, validates :path,
uniqueness: { case_sensitive: false },
presence: true,
length: { within: 1..255 }, length: { within: 1..255 },
exclusion: { in: Gitlab::Blacklist.path }, namespace: true,
format: { with: Gitlab::Regex.namespace_regex, presence: true,
message: Gitlab::Regex.namespace_regex_message } uniqueness: { case_sensitive: false }
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# system :boolean default(FALSE), not null # system :boolean default(FALSE), not null
# st_diff :text # st_diff :text
# updated_by_id :integer # updated_by_id :integer
# is_award :boolean default(FALSE), not null
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -39,9 +40,12 @@ class Note < ActiveRecord::Base ...@@ -39,9 +40,12 @@ class Note < ActiveRecord::Base
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } 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 # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -348,4 +352,31 @@ class Note < ActiveRecord::Base ...@@ -348,4 +352,31 @@ class Note < ActiveRecord::Base
def editable? def editable?
!system? !system?
end 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 end
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
# import_type :string(255) # import_type :string(255)
# import_source :string(255) # import_source :string(255)
# commit_count :integer default(0) # commit_count :integer default(0)
# import_error :text
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -152,7 +153,7 @@ class Project < ActiveRecord::Base ...@@ -152,7 +153,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id
validates :import_url, 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? if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create validate :check_limit, on: :create
......
...@@ -23,10 +23,7 @@ class BambooService < CiService ...@@ -23,10 +23,7 @@ class BambooService < CiService
prop_accessor :bamboo_url, :build_key, :username, :password prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, validates :bamboo_url, presence: true, url: true, if: :activated?
presence: true,
format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
validates :build_key, presence: true, if: :activated? validates :build_key, presence: true, if: :activated?
validates :username, validates :username,
presence: true, presence: true,
...@@ -84,7 +81,7 @@ class BambooService < CiService ...@@ -84,7 +81,7 @@ class BambooService < CiService
def supported_events def supported_events
%w(push) %w(push)
end end
def build_info(sha) def build_info(sha)
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
......
...@@ -19,14 +19,11 @@ ...@@ -19,14 +19,11 @@
# #
class DroneCiService < CiService class DroneCiService < CiService
prop_accessor :drone_url, :token, :enable_ssl_verification prop_accessor :drone_url, :token, :enable_ssl_verification
validates :drone_url,
presence: true, validates :drone_url, presence: true, url: true, if: :activated?
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? validates :token, presence: true, if: :activated?
validates :token,
presence: true,
if: :activated?
after_save :compose_service_hook, if: :activated? after_save :compose_service_hook, if: :activated?
...@@ -58,16 +55,16 @@ class DroneCiService < CiService ...@@ -58,16 +55,16 @@ class DroneCiService < CiService
end end
def merge_request_status_path(iid, sha = nil, ref = nil) def merge_request_status_path(iid, sha = nil, ref = nil)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
"?access_token=#{token}"] "?access_token=#{token}"]
URI.join(*url).to_s URI.join(*url).to_s
end end
def commit_status_path(sha, ref) def commit_status_path(sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"] "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s URI.join(*url).to_s
...@@ -114,15 +111,15 @@ class DroneCiService < CiService ...@@ -114,15 +111,15 @@ class DroneCiService < CiService
end end
def merge_request_page(iid, sha, ref) def merge_request_page(iid, sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
URI.join(*url).to_s URI.join(*url).to_s
end end
def commit_page(sha, ref) def commit_page(sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"] "?branch=#{URI::encode(ref.to_s)}"]
URI.join(*url).to_s URI.join(*url).to_s
...@@ -163,10 +160,10 @@ class DroneCiService < CiService ...@@ -163,10 +160,10 @@ class DroneCiService < CiService
end end
def push_valid?(data) def push_valid?(data)
opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id, opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
source_branch: Gitlab::Git.ref_name(data[:ref])) source_branch: Gitlab::Git.ref_name(data[:ref]))
opened_merge_requests.empty? && data[:total_commits_count] > 0 && opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
!Gitlab::Git.blank_ref?(data[:after]) !Gitlab::Git.blank_ref?(data[:after])
end end
......
...@@ -22,10 +22,8 @@ class ExternalWikiService < Service ...@@ -22,10 +22,8 @@ class ExternalWikiService < Service
include HTTParty include HTTParty
prop_accessor :external_wiki_url prop_accessor :external_wiki_url
validates :external_wiki_url,
presence: true, validates :external_wiki_url, presence: true, url: true, if: :activated?
format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
def title def title
'External Wiki' 'External Wiki'
......
...@@ -23,16 +23,16 @@ class TeamcityService < CiService ...@@ -23,16 +23,16 @@ class TeamcityService < CiService
prop_accessor :teamcity_url, :build_type, :username, :password prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, validates :teamcity_url, presence: true, url: true, if: :activated?
presence: true,
format: { with: /\A#{URI.regexp}\z/ }, if: :activated?
validates :build_type, presence: true, if: :activated? validates :build_type, presence: true, if: :activated?
validates :username, validates :username,
presence: true, presence: true,
if: ->(service) { service.password? }, if: :activated? if: ->(service) { service.password? },
if: :activated?
validates :password, validates :password,
presence: true, presence: true,
if: ->(service) { service.username? }, if: :activated? if: ->(service) { service.username? },
if: :activated?
attr_accessor :response attr_accessor :response
......
require 'securerandom' require 'securerandom'
class Repository class Repository
class PreReceiveError < StandardError; end
class CommitError < StandardError; end class CommitError < StandardError; end
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
...@@ -101,17 +100,26 @@ class Repository ...@@ -101,17 +100,26 @@ class Repository
end end
def find_branch(name) def find_branch(name)
branches.find { |branch| branch.name == name } raw_repository.branches.find { |branch| branch.name == name }
end end
def find_tag(name) def find_tag(name)
tags.find { |tag| tag.name == name } raw_repository.tags.find { |tag| tag.name == name }
end end
def add_branch(branch_name, ref) def add_branch(user, branch_name, target)
expire_branches_cache oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
target = commit(target).try(:id)
return false unless target
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
rugged.branches.create(branch_name, target)
end
gitlab_shell.add_branch(path_with_namespace, branch_name, ref) expire_branches_cache
find_branch(branch_name)
end end
def add_tag(tag_name, ref, message = nil) def add_tag(tag_name, ref, message = nil)
...@@ -120,10 +128,20 @@ class Repository ...@@ -120,10 +128,20 @@ class Repository
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end end
def rm_branch(branch_name) def rm_branch(user, branch_name)
expire_branches_cache expire_branches_cache
gitlab_shell.rm_branch(path_with_namespace, branch_name) branch = find_branch(branch_name)
oldrev = branch.try(:target)
newrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
rugged.branches.delete(branch_name)
end
expire_branches_cache
true
end end
def rm_tag(tag_name) def rm_tag(tag_name)
...@@ -311,6 +329,17 @@ class Repository ...@@ -311,6 +329,17 @@ class Repository
commit(sha) commit(sha)
end end
def next_patch_branch
patch_branch_ids = self.branch_names.map do |n|
result = n.match(/\Apatch-([0-9]+)\z/)
result[1].to_i if result
end.compact
highest_patch_branch_id = patch_branch_ids.max || 0
"patch-#{highest_patch_branch_id + 1}"
end
# Remove archives older than 2 hours # Remove archives older than 2 hours
def branches_sorted_by(value) def branches_sorted_by(value)
case value case value
...@@ -550,7 +579,6 @@ class Repository ...@@ -550,7 +579,6 @@ class Repository
def commit_with_hooks(current_user, branch) def commit_with_hooks(current_user, branch)
oldrev = Gitlab::Git::BLANK_SHA oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
gl_id = Gitlab::ShellEnv.gl_id(current_user)
was_empty = empty? was_empty = empty?
# Create temporary ref # Create temporary ref
...@@ -569,15 +597,7 @@ class Repository ...@@ -569,15 +597,7 @@ class Repository
raise CommitError.new('Failed to create commit') raise CommitError.new('Failed to create commit')
end end
# Run GitLab pre-receive hook GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo)
pre_receive_hook_status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref)
# Run GitLab update hook
update_hook = Gitlab::Git::Hook.new('update', path_to_repo)
update_hook_status = update_hook.trigger(gl_id, oldrev, newrev, ref)
if pre_receive_hook_status && update_hook_status
if was_empty if was_empty
# Create branch # Create branch
rugged.references.create(ref, newrev) rugged.references.create(ref, newrev)
...@@ -592,16 +612,11 @@ class Repository ...@@ -592,16 +612,11 @@ class Repository
raise CommitError.new('Commit was rejected because branch received new push') raise CommitError.new('Commit was rejected because branch received new push')
end end
end end
# Run GitLab post receive hook
post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
else
# Remove tmp ref and return error to user
rugged.references.delete(tmp_ref)
raise PreReceiveError.new('Commit was rejected by git hook')
end end
rescue GitHooksService::PreReceiveError
# Remove tmp ref and return error to user
rugged.references.delete(tmp_ref)
raise
end end
private private
......
...@@ -21,7 +21,7 @@ class SentNotification < ActiveRecord::Base ...@@ -21,7 +21,7 @@ class SentNotification < ActiveRecord::Base
validates :reply_key, uniqueness: true validates :reply_key, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit? validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit? validates :commit_id, presence: true, if: :for_commit?
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true validates :line_code, line_code: true, allow_blank: true
class << self class << self
def reply_key def reply_key
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
# project_view :integer default(0) # project_view :integer default(0)
# consumed_timestep :integer # consumed_timestep :integer
# layout :integer default(0) # layout :integer default(0)
# hide_project_limit :boolean default(FALSE)
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -148,11 +149,9 @@ class User < ActiveRecord::Base ...@@ -148,11 +149,9 @@ class User < ActiveRecord::Base
validates :bio, length: { maximum: 255 }, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username, validates :username,
namespace: true,
presence: true, presence: true,
uniqueness: { case_sensitive: false }, uniqueness: { case_sensitive: false }
exclusion: { in: Gitlab::Blacklist.path },
format: { with: Gitlab::Regex.namespace_regex,
message: Gitlab::Regex.namespace_regex_message }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :namespace_uniq, if: ->(user) { user.username_changed? }
......
...@@ -13,8 +13,7 @@ class CreateBranchService < BaseService ...@@ -13,8 +13,7 @@ class CreateBranchService < BaseService
return error('Branch already exists') return error('Branch already exists')
end end
repository.add_branch(branch_name, ref) new_branch = repository.add_branch(current_user, branch_name, ref)
new_branch = repository.find_branch(branch_name)
if new_branch if new_branch
push_data = build_push_data(project, current_user, new_branch) push_data = build_push_data(project, current_user, new_branch)
...@@ -27,6 +26,8 @@ class CreateBranchService < BaseService ...@@ -27,6 +26,8 @@ class CreateBranchService < BaseService
else else
error('Invalid reference name') error('Invalid reference name')
end end
rescue GitHooksService::PreReceiveError
error('Branch creation was rejected by Git hook')
end end
def success(branch) def success(branch)
......
...@@ -24,7 +24,7 @@ class DeleteBranchService < BaseService ...@@ -24,7 +24,7 @@ class DeleteBranchService < BaseService
return error('You dont have push access to repo', 405) return error('You dont have push access to repo', 405)
end end
if repository.rm_branch(branch_name) if repository.rm_branch(current_user, branch_name)
push_data = build_push_data(branch) push_data = build_push_data(branch)
EventCreateService.new.push(project, current_user, push_data) EventCreateService.new.push(project, current_user, push_data)
...@@ -35,6 +35,8 @@ class DeleteBranchService < BaseService ...@@ -35,6 +35,8 @@ class DeleteBranchService < BaseService
else else
error('Failed to remove branch') error('Failed to remove branch')
end end
rescue GitHooksService::PreReceiveError
error('Branch deletion was rejected by Git hook')
end end
def error(message, return_code = 400) def error(message, return_code = 400)
......
...@@ -26,7 +26,7 @@ module Files ...@@ -26,7 +26,7 @@ module Files
else else
error("Something went wrong. Your changes were not committed") error("Something went wrong. Your changes were not committed")
end end
rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message) error(ex.message)
end end
...@@ -53,7 +53,7 @@ module Files ...@@ -53,7 +53,7 @@ module Files
unless project.empty_repo? unless project.empty_repo?
unless repository.branch_names.include?(@current_branch) unless repository.branch_names.include?(@current_branch)
raise_error("You can only create files if you are on top of a branch") raise_error("You can only create or edit files when you are on a branch")
end end
if @current_branch != @target_branch if @current_branch != @target_branch
......
class GitHooksService
PreReceiveError = Class.new(StandardError)
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
@user = Gitlab::ShellEnv.gl_id(user)
@oldrev = oldrev
@newrev = newrev
@ref = ref
%w(pre-receive update).each do |hook_name|
unless run_hook(hook_name)
raise PreReceiveError.new("Git operation was rejected by #{hook_name} hook")
end
end
yield
run_hook('post-receive')
end
private
def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path)
hook.trigger(@user, @oldrev, @newrev, @ref)
end
end
...@@ -6,15 +6,12 @@ module MergeRequests ...@@ -6,15 +6,12 @@ module MergeRequests
# Executed when you do merge via GitLab UI # Executed when you do merge via GitLab UI
# #
class MergeService < MergeRequests::BaseService class MergeService < MergeRequests::BaseService
attr_reader :merge_request, :commit_message attr_reader :merge_request
def execute(merge_request, commit_message) def execute(merge_request)
@commit_message = commit_message
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? return error('Merge request is not mergeable') unless @merge_request.mergeable?
return error('Merge request is not mergeable')
end
merge_request.in_locked_state do merge_request.in_locked_state do
if commit if commit
...@@ -32,7 +29,7 @@ module MergeRequests ...@@ -32,7 +29,7 @@ module MergeRequests
committer = repository.user_to_committer(current_user) committer = repository.user_to_committer(current_user)
options = { options = {
message: commit_message, message: params[:commit_message] || merge_request.merge_commit_message,
author: committer, author: committer,
committer: committer committer: committer
} }
...@@ -46,6 +43,11 @@ module MergeRequests ...@@ -46,6 +43,11 @@ module MergeRequests
def after_merge def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch]
DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch)
end
end end
end end
end end
module MergeRequests
class MergeWhenBuildSucceedsService < MergeRequests::BaseService
# Marks the passed `merge_request` to be merged when the build succeeds or
# updates the params for the automatic merge
def execute(merge_request)
merge_request.merge_params.merge!(params)
# The service is also called when the merge params are updated.
already_approved = merge_request.merge_when_build_succeeds?
unless already_approved
merge_request.merge_when_build_succeeds = true
merge_request.merge_user = @current_user
SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit)
end
merge_request.save
end
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(build)
merge_requests = merge_request_from(build)
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
end
# Cancels the automatic merge
def cancel(merge_request)
if merge_request.merge_when_build_succeeds? && merge_request.open?
merge_request.reset_merge_when_build_succeeds
SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
success
else
error("Can't cancel the automatic merge", 406)
end
end
private
def merge_request_from(build)
merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a
merge_requests.uniq.select(&:source_project)
end
end
end
...@@ -11,6 +11,7 @@ module MergeRequests ...@@ -11,6 +11,7 @@ module MergeRequests
# empty diff during a manual merge # empty diff during a manual merge
close_merge_requests close_merge_requests
reload_merge_requests reload_merge_requests
reset_merge_when_build_succeeds
# Leave a system note if a branch was deleted/added # Leave a system note if a branch was deleted/added
if branch_added? || branch_removed? if branch_added? || branch_removed?
...@@ -57,7 +58,6 @@ module MergeRequests ...@@ -57,7 +58,6 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests) merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
if merge_request.source_branch == @branch_name || force_push? if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_code merge_request.reload_code
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
...@@ -76,6 +76,10 @@ module MergeRequests ...@@ -76,6 +76,10 @@ module MergeRequests
end end
end end
def reset_merge_when_build_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
end
def find_new_commits def find_new_commits
if branch_added? if branch_added?
@commits = [] @commits = []
......
...@@ -5,11 +5,6 @@ module Notes ...@@ -5,11 +5,6 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if contains_emoji_only?(params[:note])
note.is_award = true
note.note = emoji_name(params[:note])
end
if note.save if note.save
notification_service.new_note(note) notification_service.new_note(note)
...@@ -33,13 +28,5 @@ module Notes ...@@ -33,13 +28,5 @@ module Notes
note.project.execute_hooks(note_data, :note_hooks) note.project.execute_hooks(note_data, :note_hooks)
note.project.execute_services(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks)
end end
def contains_emoji_only?(note)
note =~ /\A:[-_+[:alnum:]]*:\s?\z/
end
def emoji_name(note)
note.match(/\A:([-_+[:alnum:]]*):\s?/)[1]
end
end end
end end
...@@ -130,6 +130,20 @@ class SystemNoteService ...@@ -130,6 +130,20 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
# Called when 'merge when build succeeds' is executed
def self.merge_when_build_succeeds(noteable, project, author, last_commit)
body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when 'merge when build succeeds' is canceled
def self.cancel_merge_when_build_succeeds(noteable, project, author)
body = "Canceled the automatic merge"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the title of a Noteable is changed # Called when the title of a Noteable is changed
# #
# noteable - Noteable object that responds to `title` # noteable - Noteable object that responds to `title`
......
# ColorValidator
#
# Custom validator for web color codes. It requires the leading hash symbol and
# will accept RGB triplet or hexadecimal formats.
#
# Example:
#
# class User < ActiveRecord::Base
# validates :background_color, allow_blank: true, color: true
# end
#
class ColorValidator < ActiveModel::EachValidator
PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze
def validate_each(record, attribute, value)
unless value =~ PATTERN
record.errors.add(attribute, "must be a valid color code")
end
end
end
# EmailValidator
#
# Based on https://github.com/balexand/email_validator # Based on https://github.com/balexand/email_validator
# #
# Extended to use only strict mode with following allowed characters: # Extended to use only strict mode with following allowed characters:
...@@ -6,15 +8,10 @@ ...@@ -6,15 +8,10 @@
# See http://www.remote.org/jochen/mail/info/chars.html # See http://www.remote.org/jochen/mail/info/chars.html
# #
class EmailValidator < ActiveModel::EachValidator class EmailValidator < ActiveModel::EachValidator
@@default_options = {} PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze
def self.default_options
@@default_options
end
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
options = @@default_options.merge(self.options) unless value =~ PATTERN
unless value =~ /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i
record.errors.add(attribute, options[:message] || :invalid) record.errors.add(attribute, options[:message] || :invalid)
end end
end end
......
# LineCodeValidator
#
# Custom validator for GitLab line codes.
class LineCodeValidator < ActiveModel::EachValidator
PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze
def validate_each(record, attribute, value)
unless value =~ PATTERN
record.errors.add(attribute, "must be a valid line code")
end
end
end
# NamespaceNameValidator
#
# Custom validator for GitLab namespace name strings.
class NamespaceNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_name_regex
record.errors.add(attribute, Gitlab::Regex.namespace_name_regex_message)
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
RESERVED = %w(
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
notes
profile
projects
public
repository
s
search
services
snippets
teams
u
unsubscribes
users
).freeze
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
private
def reserved?(value)
RESERVED.include?(value)
end
end
# UrlValidator
#
# Custom validator for URLs.
#
# By default, only URLs for the HTTP(S) protocols will be considered valid.
# Provide a `:protocols` option to configure accepted protocols.
#
# Example:
#
# class User < ActiveRecord::Base
# validates :personal_url, url: true
#
# validates :ftp_url, url: { protocols: %w(ftp) }
#
# validates :git_url, url: { protocols: %w(http https ssh git) }
# end
#
class UrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless valid_url?(value)
record.errors.add(attribute, "must be a valid URL")
end
end
private
def default_options
@default_options ||= { protocols: %w(http https) }
end
def valid_url?(value)
options = default_options.merge(self.options)
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
...@@ -14,11 +14,11 @@ ...@@ -14,11 +14,11 @@
.form-group.project-visibility-level-holder .form-group.project-visibility-level-holder
= f.label :default_project_visibility, class: 'control-label col-sm-2' = f.label :default_project_visibility, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project') = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project)
.form-group.project-visibility-level-holder .form-group.project-visibility-level-holder
= f.label :default_snippet_visibility, class: 'control-label col-sm-2' = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet') = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet)
.form-group .form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2' = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= render 'shared/project_limit' = render 'shared/project_limit'
%ul.center-top-menu %ul.center-top-menu
= nav_link(path: ['projects#index', 'root#index']) do = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects Your Projects
= nav_link(page: starred_dashboard_projects_path) do = nav_link(page: starred_dashboard_projects_path) do
......
...@@ -4,14 +4,20 @@ ...@@ -4,14 +4,20 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
.project-issuable-filter
.controls
.pull-left
- if current_user
.hidden-xs.pull-left
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
%i.fa.fa-rss
.append-bottom-20 = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
.pull-right
- if current_user
.hidden-xs.pull-left.prepend-top-20
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: '' do
%i.fa.fa-rss
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
= render 'shared/issues' .gray-content-block.second-block
List all issues from all projects you have access to.
.prepend-top-default
= render 'shared/issues'
- page_title "Merge Requests" - page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
.append-bottom-20 .project-issuable-filter
.controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
= render 'shared/merge_requests'
.gray-content-block.second-block
List all merge requests from all projects you have access to.
.prepend-top-default
= render 'shared/merge_requests'
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
.row .row
.col-sm-6 .col-sm-6
- milestone.milestones.each do |milestone| .expiration
= link_to milestone_path(milestone) do = render 'shared/milestone_expired', milestone: milestone
%span.label.label-gray .projects
= milestone.project.name_with_namespace - milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= milestone.project.name_with_namespace
- page_title "Milestones" - page_title "Milestones"
- header_title "Milestones", dashboard_milestones_path - header_title "Milestones", dashboard_milestones_path
.project-issuable-filter
.controls
= render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
= render 'shared/milestones_filter' = render 'shared/milestones_filter'
.gray-content-block .gray-content-block
.oneline List all milestones from all projects you have access to.
List all milestones from all projects you have access to.
.milestones .milestones
%ul.content-list %ul.content-list
......
...@@ -4,21 +4,24 @@ ...@@ -4,21 +4,24 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
.project-issuable-filter
.controls
.pull-left
- if current_user
.hidden-xs.pull-left
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
%i.fa.fa-rss
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block .gray-content-block.second-block
.pull-right Only issues from
- if current_user %strong #{@group.name}
.hidden-xs.pull-left group are listed here.
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do - if current_user
%i.fa.fa-rss To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
%div
Only issues from
%strong #{@group.name}
group are listed here.
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
.prepend-top-default .prepend-top-default
= render 'shared/issues' = render 'shared/issues'
- page_title "Merge Requests" - page_title "Merge Requests"
- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group)) - header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group))
= render 'shared/issuable/filter', type: :merge_requests .project-issuable-filter
.controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
.gray-content-block.second-block .gray-content-block.second-block
%div Only merge requests from
Only merge requests from %strong #{@group.name}
%strong #{@group.name} group are listed here.
group are listed here. - if current_user
- if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
.prepend-top-default .prepend-top-default
= render 'shared/merge_requests' = render 'shared/merge_requests'
- page_title "Milestones" - page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group)) - header_title group_title(@group, "Milestones", group_milestones_path(@group))
= render 'shared/milestones_filter' .project-issuable-filter
.controls
- if can?(current_user, :admin_milestones, @group)
.pull-right
%span.pull-right.hidden-xs
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
= icon('plus')
New Milestone
= render 'shared/milestones_filter'
.gray-content-block .gray-content-block
- if can?(current_user, :admin_milestones, @group) Only milestones from
.pull-right %strong #{@group.name}
%span.pull-right.hidden-xs group are listed here.
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
New Milestone
.oneline
Only milestones from
%strong #{@group.name}
group are listed here.
.milestones .milestones
%ul.content-list %ul.content-list
- if @milestones.blank? - if @milestones.blank?
......
...@@ -31,11 +31,9 @@ ...@@ -31,11 +31,9 @@
%h2#blocks Blocks %h2#blocks Blocks
%h3 %h4
%code .gray-content-block %code .gray-content-block
.gray-content-block.middle-block .gray-content-block.middle-block
%h4 Normal block inside content %h4 Normal block inside content
= lorem = lorem
...@@ -45,9 +43,28 @@ ...@@ -45,9 +43,28 @@
= lorem = lorem
%h4
%code .cover-block
%br
.cover-block
.avatar-holder
= image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title
John Smith
.cover-desc
= lorem
.cover-controls
= link_to '#', class: 'btn btn-gray' do
= icon('pencil')
&nbsp;
= link_to '#', class: 'btn btn-gray' do
= icon('rss')
%h2#lists Lists %h2#lists Lists
%h3 %h4
%code .content-list %code .content-list
%ul.content-list %ul.content-list
%li %li
...@@ -57,7 +74,7 @@ ...@@ -57,7 +74,7 @@
%li %li
One item One item
%h3 %h4
%code .well-list %code .well-list
%ul.well-list %ul.well-list
%li %li
...@@ -67,7 +84,7 @@ ...@@ -67,7 +84,7 @@
%li %li
One item One item
%h3 %h4
%code .panel .well-list %code .panel .well-list
.panel.panel-default .panel.panel-default
...@@ -80,7 +97,7 @@ ...@@ -80,7 +97,7 @@
%li %li
One item One item
%h3 %h4
%code .bordered-list %code .bordered-list
%ul.bordered-list %ul.bordered-list
%li %li
...@@ -121,7 +138,7 @@ ...@@ -121,7 +138,7 @@
%h2#navs Navigation %h2#navs Navigation
%h3 %h4
%code .center-top-menu %code .center-top-menu
.example .example
%ul.center-top-menu %ul.center-top-menu
...@@ -130,7 +147,7 @@ ...@@ -130,7 +147,7 @@
%li %li
%a Closed %a Closed
%h3 %h4
%code .btn-group.btn-group-next %code .btn-group.btn-group-next
.example .example
%div.btn-group.btn-group-next %div.btn-group.btn-group-next
...@@ -138,7 +155,7 @@ ...@@ -138,7 +155,7 @@
%a.btn Closed %a.btn Closed
%h3 %h4
%code .nav.nav-tabs %code .nav.nav-tabs
.example .example
%ul.nav.nav-tabs %ul.nav.nav-tabs
...@@ -204,7 +221,7 @@ ...@@ -204,7 +221,7 @@
%h2#forms Forms %h2#forms Forms
%h3 %h4
%code form.horizontal-form %code form.horizontal-form
%form.form-horizontal %form.form-horizontal
...@@ -226,7 +243,7 @@ ...@@ -226,7 +243,7 @@
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
%button.btn.btn-default{:type => "submit"} Sign in %button.btn.btn-default{:type => "submit"} Sign in
%h3 %h4
%code form %code form
%form %form
...@@ -243,7 +260,7 @@ ...@@ -243,7 +260,7 @@
%button.btn.btn-default{:type => "submit"} Sign in %button.btn.btn-default{:type => "submit"} Sign in
%h2#file File %h2#file File
%h3 %h4
%code .file-holder %code .file-holder
- blob = Snippet.new(content: "Wow\nSuch\nFile") - blob = Snippet.new(content: "Wow\nSuch\nFile")
...@@ -254,13 +271,12 @@ ...@@ -254,13 +271,12 @@
.file-actions .file-actions
.btn-group .btn-group
%a.btn Edit %a.btn Edit
%a.btn Remove %a.btn.btn-danger Remove
.file-contenta.code .file-contenta.code
= render 'shared/file_highlight', blob: blob = render 'shared/file_highlight', blob: blob
%h2#markdown Markdown %h2#markdown Markdown
%h3 %h4
%code .md or .wiki and others %code .md or .wiki and others
Markdown rendering has a bit different css and presented in next UI elements: Markdown rendering has a bit different css and presented in next UI elements:
......
...@@ -26,11 +26,11 @@ ...@@ -26,11 +26,11 @@
- else - else
%span You don`t have one yet. Click generate to fix it. %span You don`t have one yet. Click generate to fix it.
.form-actions .form-actions
- if current_user.private_token - if current_user.private_token
= f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
- else - else
= f.submit 'Generate', class: "btn btn-default btn-build-token" = f.submit 'Generate', class: "btn btn-default"
- unless current_user.ldap_user? - unless current_user.ldap_user?
.panel.panel-default .panel.panel-default
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
$('#key_key').on('focusout', function(){ $('#key_key').on('focusout', function(){
var title = $('#key_title'), var title = $('#key_title'),
val = $('#key_key').val(), val = $('#key_key').val(),
comment = val.match(/^\S+ \S+ (.+)$/); comment = val.match(/^\S+ \S+ (.+)\n?$/);
if( comment && comment.length > 1 && title.val() == '' ){ if( comment && comment.length > 1 && title.val() == '' ){
$('#key_title').val( comment[1] ); $('#key_title').val( comment[1] );
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- if ci_commit - if ci_commit
= link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do
= ci_status_icon(ci_commit) = ci_status_icon(ci_commit)
= ci_commit.status = ci_status_label(ci_commit)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
......
...@@ -8,17 +8,18 @@ ...@@ -8,17 +8,18 @@
%a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
Preview Preview
- if defined?(referenced_users) && referenced_users %div
%span.referenced-users.pull-left.hide .md-write-holder
= yield
.md.md-preview-holder.hide
.js-md-preview{class: (preview_class if defined?(preview_class))}
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
%span
= icon('exclamation-triangle') = icon('exclamation-triangle')
You are about to add You are about to add
%strong %strong
%span.js-referenced-users-count 0 %span.js-referenced-users-count 0
people people
to the discussion. Proceed with caution. to the discussion. Proceed with caution.
%div
.md-write-holder
= yield
.md.md-preview-holder.hide
.js-md-preview{class: (preview_class if defined?(preview_class))}
.btn-group.tree-btn-group .btn-group.tree-btn-group
= edit_blob_link(@project, @ref, @path)
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank' class: 'btn btn-sm', target: '_blank'
-# only show normal/blame view links for text files -# only show normal/blame view links for text files
- if @blob.text? - if blob_viewable?(@blob)
- if current_page? namespace_project_blame_path(@project.namespace, @project, @id) - if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
= link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn btn-sm' class: 'btn btn-sm'
...@@ -12,11 +11,16 @@ ...@@ -12,11 +11,16 @@
class: 'btn btn-sm' unless @blob.empty? class: 'btn btn-sm' unless @blob.empty?
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm' class: 'btn btn-sm'
- if @ref != @commit.sha = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn btn-sm'
tree_join(@commit.sha, @path)), class: 'btn btn-sm'
- if allowed_tree_edit? - if blob_editable?(@blob)
.btn-group{ role: "group" } .btn-group{ role: "group" }
= edit_blob_link(@project, @ref, @path)
%button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
%button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete
- elsif !on_top_of_branch?
.btn-group{ role: "group" }
%button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on a branch.", data: {container: 'body'}} Edit
%button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on a branch.", data: {container: 'body'}} Replace
%button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on a branch.", data: {container: 'body'}} Delete
...@@ -29,10 +29,12 @@ ...@@ -29,10 +29,12 @@
%strong %strong
= blob.name = blob.name
%small %small
= number_to_human_size(blob.size) = number_to_human_size(blob_size(blob))
.file-actions.hidden-xs .file-actions.hidden-xs
= render "actions" = render "actions"
- if blob.text? - if blob.lfs_pointer?
= render "download", blob: blob
- elsif blob.text?
= render "text", blob: blob = render "text", blob: blob
- elsif blob.image? - elsif blob.image?
= render "image", blob: blob = render "image", blob: blob
......
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
%h1.light %h1.light
%i.fa.fa-download %i.fa.fa-download
%h4 %h4
Download (#{number_to_human_size blob.size}) Download (#{number_to_human_size blob_size(blob)})
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