Commit b65f4c99 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce_upstream' into 'master'

See merge request !107
parents 0a7bea9c 2ba660e4
...@@ -26,6 +26,7 @@ config/initializers/smtp_settings.rb ...@@ -26,6 +26,7 @@ config/initializers/smtp_settings.rb
config/resque.yml config/resque.yml
config/unicorn.rb config/unicorn.rb
config/secrets.yml config/secrets.yml
config/sidekiq.yml
coverage/* coverage/*
db/*.sqlite3 db/*.sqlite3
db/*.sqlite3-journal db/*.sqlite3-journal
......
...@@ -12,6 +12,7 @@ before_script: ...@@ -12,6 +12,7 @@ before_script:
spec:feature: spec:feature:
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags: tags:
- ruby - ruby
......
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.4.0 (unreleased) v 8.4.0 (unreleased)
- Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse)
- Improved performance of finding issues for an entire group (Yorick Peterse)
- Added custom application performance measuring system powered by InfluxDB (Yorick Peterse)
- Bump fog to 1.36.0 (Stan Hu)
- Add user's last used IP addresses to admin page (Stan Hu)
- Add housekeeping function to project settings page - Add housekeeping function to project settings page
- The default GitLab logo now acts as a loading indicator - The default GitLab logo now acts as a loading indicator
- Fix caching issue where build status was not updating in project dashboard (Stan Hu) - Fix caching issue where build status was not updating in project dashboard (Stan Hu)
...@@ -8,6 +13,7 @@ v 8.4.0 (unreleased) ...@@ -8,6 +13,7 @@ v 8.4.0 (unreleased)
- Fix missing date of month in network graph when commits span a month (Stan Hu) - Fix missing date of month in network graph when commits span a month (Stan Hu)
- Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
- Don't notify users twice if they are both project watchers and subscribers (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
- Fix error with file size check with submodules (Stan Hu)
- Implement new UI for group page - Implement new UI for group page
- Implement search inside emoji picker - Implement search inside emoji picker
- Add API support for looking up a user by username (Stan Hu) - Add API support for looking up a user by username (Stan Hu)
...@@ -38,6 +44,9 @@ v 8.4.0 (unreleased) ...@@ -38,6 +44,9 @@ v 8.4.0 (unreleased)
- API: Add support for deleting a tag via the API (Robert Schilling) - API: Add support for deleting a tag via the API (Robert Schilling)
- Allow subsequent validations in CI Linter - Allow subsequent validations in CI Linter
v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
v 8.3.3 v 8.3.3
- Preserve CE behavior with JIRA integration by only calling API if URL is set - Preserve CE behavior with JIRA integration by only calling API if URL is set
- Fix duplicated branch creation/deletion events when using Web UI (Stan Hu) - Fix duplicated branch creation/deletion events when using Web UI (Stan Hu)
......
...@@ -155,6 +155,28 @@ sudo -u git -H bundle exec rake gitlab:env:info) ...@@ -155,6 +155,28 @@ sudo -u git -H bundle exec rake gitlab:env:info)
``` ```
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
one or multiple issues. This makes it possible to schedule work more accurately.
You are encouraged to set the weight of any issue. Following the guidelines
below will make it easy to manage this, without unnecessary overhead.
1. Set weight for any issue at the earliest possible convenience
1. If you don't agree with a set weight, discuss with other developers until
consensus is reached about the weight
1. Issue weights are an abstract measurement of complexity of the issue. Do not
relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring)
and something you want to avoid.
1. Something that has a weight of 1 (or no weight) is really small and simple.
Something that is 9 is rewriting a large fundamental part of GitLab,
which might lead to many hard problems to solve. Changing some text in GitLab
is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
1. If something is very large, it should probably be split up in multiple
issues or chunks. You can simply not set the weight of a parent issue and set
weights to children issues.
## Merge requests ## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests, We welcome merge requests with fixes and improvements to GitLab code, tests,
......
...@@ -22,6 +22,7 @@ gem 'devise', '~> 3.5.3' ...@@ -22,6 +22,7 @@ gem 'devise', '~> 3.5.3'
gem 'devise-async', '~> 0.9.0' gem 'devise-async', '~> 0.9.0'
gem 'doorkeeper', '~> 2.2.0' gem 'doorkeeper', '~> 2.2.0'
gem 'omniauth', '~> 1.2.2' gem 'omniauth', '~> 1.2.2'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 3.0.0' gem 'omniauth-facebook', '~> 3.0.0'
...@@ -32,10 +33,13 @@ gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos ...@@ -32,10 +33,13 @@ gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-saml', '~> 1.4.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd' gem 'omniauth_crowd', '~> 2.2.0'
gem 'gssapi', group: :kerberos gem 'gssapi', group: :kerberos
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
# reCAPTCHA protection
gem 'recaptcha', require: 'recaptcha/rails'
# Two-factor authentication # Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0' gem 'devise-two-factor', '~> 2.0.0'
gem 'rqrcode-rails3', '~> 0.1.7' gem 'rqrcode-rails3', '~> 0.1.7'
...@@ -46,7 +50,7 @@ gem "browser", '~> 1.0.0' ...@@ -46,7 +50,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem "gitlab_git", '~> 7.2.20' gem "gitlab_git", '~> 7.2.22'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
...@@ -65,13 +69,6 @@ gem 'grape', '~> 0.13.0' ...@@ -65,13 +69,6 @@ gem 'grape', '~> 0.13.0'
gem 'grape-entity', '~> 0.4.2' gem 'grape-entity', '~> 0.4.2'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Format dates and times
# based on human-friendly examples
gem "stamp", '~> 0.6.0'
# Enumeration fields
gem 'enumerize', '~> 0.7.0'
# Pagination # Pagination
gem "kaminari", "~> 0.16.3" gem "kaminari", "~> 0.16.3"
...@@ -85,7 +82,7 @@ gem "carrierwave", '~> 0.9.0' ...@@ -85,7 +82,7 @@ gem "carrierwave", '~> 0.9.0'
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for aws storage # for aws storage
gem "fog", "~> 1.25.0" gem "fog", "~> 1.36.0"
gem "unf", '~> 0.1.4' gem "unf", '~> 0.1.4'
# Authorization # Authorization
...@@ -171,10 +168,10 @@ gem 'asana', '~> 0.4.0' ...@@ -171,10 +168,10 @@ gem 'asana', '~> 0.4.0'
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
# d3 # d3
gem 'd3_rails', '~> 3.5.5' gem 'd3_rails', '~> 3.5.0'
#cal-heatmap #cal-heatmap
gem "cal-heatmap-rails", "~> 0.0.1" gem 'cal-heatmap-rails', '~> 3.5.0'
# underscore-rails # underscore-rails
gem "underscore-rails", "~> 1.8.0" gem "underscore-rails", "~> 1.8.0"
...@@ -202,7 +199,7 @@ gem 'turbolinks', '~> 2.5.0' ...@@ -202,7 +199,7 @@ gem 'turbolinks', '~> 2.5.0'
gem 'jquery-turbolinks', '~> 2.1.0' gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2' gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.2.0' gem 'gitlab_emoji', '~> 0.2.0'
gem 'gon', '~> 6.0.1' gem 'gon', '~> 6.0.1'
...@@ -219,9 +216,17 @@ gem 'net-ssh', '~> 3.0.1' ...@@ -219,9 +216,17 @@ gem 'net-ssh', '~> 3.0.1'
gem "gitlab-license", "~> 0.0.4" gem "gitlab-license", "~> 0.0.4"
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
gem 'connection_pool', '~> 2.0', require: false
end
group :development do group :development do
gem "foreman" gem "foreman"
gem 'brakeman', '3.0.1', require: false gem 'brakeman', '~> 3.1.0', require: false
gem "annotate", "~> 2.6.0" gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2' gem "letter_opener", '~> 1.1.2'
......
This diff is collapsed.
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
----------------------------------------------------------- -----------------------------------------------------------
......
...@@ -10,13 +10,13 @@ ...@@ -10,13 +10,13 @@
#= require jquery.cookie #= require jquery.cookie
#= require jquery.endless-scroll #= require jquery.endless-scroll
#= require jquery.highlight #= require jquery.highlight
#= require jquery.history
#= require jquery.waitforimages #= require jquery.waitforimages
#= require jquery.atwho #= require jquery.atwho
#= require jquery.scrollTo #= require jquery.scrollTo
#= require jquery.blockUI
#= require jquery.turbolinks #= require jquery.turbolinks
#= require jquery.tablesorter #= require jquery.tablesorter
#= require d3
#= require cal-heatmap
#= require turbolinks #= require turbolinks
#= require autosave #= require autosave
#= require bootstrap #= require bootstrap
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
#= require branch-graph #= require branch-graph
#= require ace/ace #= require ace/ace
#= require ace/ext-searchbox #= require ace/ext-searchbox
#= require d3
#= require underscore #= require underscore
#= require nprogress #= require nprogress
#= require nprogress-turbolinks #= require nprogress-turbolinks
...@@ -40,9 +39,9 @@ ...@@ -40,9 +39,9 @@
#= require shortcuts_dashboard_navigation #= require shortcuts_dashboard_navigation
#= require shortcuts_issuable #= require shortcuts_issuable
#= require shortcuts_network #= require shortcuts_network
#= require cal-heatmap
#= require jquery.nicescroll.min #= require jquery.nicescroll.min
#= require_tree . #= require_tree .
#= require fuzzaldrin-plus.min
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......
...@@ -134,6 +134,7 @@ class @AwardsHandler ...@@ -134,6 +134,7 @@ class @AwardsHandler
_.compact(_.uniq(frequently_used_emojis)) _.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: -> renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis')
frequently_used_emojis = @getFrequentlyUsedEmojis() frequently_used_emojis = @getFrequentlyUsedEmojis()
ul = $("<ul>") ul = $("<ul>")
......
...@@ -66,7 +66,7 @@ class @BranchGraph ...@@ -66,7 +66,7 @@ class @BranchGraph
r.rect(40, 0, 30, @barHeight).attr fill: "#444" r.rect(40, 0, 30, @barHeight).attr fill: "#444"
for day, mm in @days for day, mm in @days
if cuday isnt day[0] if cuday isnt day[0] || cumonth isnt day[1]
# Dates # Dates
r.text(55, @offsetY + @unitTime * mm, day[0]) r.text(55, @offsetY + @unitTime * mm, day[0])
.attr( .attr(
......
class @Calendar class @Calendar
options =
month: "short"
day: "numeric"
year: "numeric"
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap() cal = new CalHeatMap()
cal.init cal.init
......
class @CommitsList class @CommitsList
@data = @timer = null
ref: null
limit: 0
offset: 0
@disable = false
@showProgress: ->
$('.loading').show()
@hideProgress: ->
$('.loading').hide()
@init: (ref, limit) -> @init: (ref, limit) ->
$("body").on "click", ".day-commits-table li.commit", (event) -> $("body").on "click", ".day-commits-table li.commit", (event) ->
...@@ -18,38 +8,32 @@ class @CommitsList ...@@ -18,38 +8,32 @@ class @CommitsList
e.stopPropagation() e.stopPropagation()
return false return false
@data.ref = ref Pager.init limit, false
@data.limit = limit
@data.offset = limit @content = $("#commits-list")
@searchField = $("#commits-search")
@initSearch()
this.initLoadMore() @initSearch: ->
this.showProgress() @timer = null
@searchField.keyup =>
clearTimeout(@timer)
@timer = setTimeout(@filterResults, 500)
@filterResults: =>
form = $(".commits-search-form")
search = @searchField.val()
commitsUrl = form.attr("action") + '?' + form.serialize()
@content.fadeTo('fast', 0.5)
@getOld: ->
this.showProgress()
$.ajax $.ajax
type: "GET" type: "GET"
url: location.href url: form.attr("action")
data: @data data: form.serialize()
complete: this.hideProgress complete: =>
success: (data) -> @content.fadeTo('fast', 1.0)
CommitsList.append(data.count, data.html) success: (data) =>
@content.html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: commitsUrl}, document.title, commitsUrl
dataType: "json" dataType: "json"
@append: (count, html) ->
$("#commits-list").append(html)
if count > 0
@data.offset += count
else
@disable = true
@initLoadMore: ->
$(document).unbind('scroll')
$(document).endlessScroll
bottomPixels: 400
fireDelay: 1000
fireOnce: true
ceaseFire: =>
@disable
callback: =>
this.getOld()
...@@ -49,7 +49,7 @@ class Dispatcher ...@@ -49,7 +49,7 @@ class Dispatcher
new DropzoneInput($('.release-form')) new DropzoneInput($('.release-form'))
when 'projects:merge_requests:show' when 'projects:merge_requests:show'
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
...@@ -87,7 +87,9 @@ class Dispatcher ...@@ -87,7 +87,9 @@ class Dispatcher
new GroupAvatar() new GroupAvatar()
when 'projects:tree:show' when 'projects:tree:show'
new TreeView() new TreeView()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsTree()
when 'projects:find_file:show'
shortcut_handler = true
when 'projects:blob:show' when 'projects:blob:show'
new LineHighlighter() new LineHighlighter()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
......
...@@ -66,7 +66,7 @@ class @DropzoneInput ...@@ -66,7 +66,7 @@ class @DropzoneInput
success: (header, response) -> success: (header, response) ->
child = $(dropzone[0]).children("textarea") child = $(dropzone[0]).children("textarea")
$(child).val $(child).val() + formatLink(response.link) + "\n" $(child).val $(child).val() + response.link.markdown + "\n"
return return
error: (temp, errorMessage) -> error: (temp, errorMessage) ->
...@@ -99,11 +99,6 @@ class @DropzoneInput ...@@ -99,11 +99,6 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea") child = $(dropzone[0]).children("textarea")
formatLink = (link) ->
text = "[#{link.alt}](#{link.url})"
text = "!#{text}" if link.is_image
text
handlePaste = (event) -> handlePaste = (event) ->
pasteEvent = event.originalEvent pasteEvent = event.originalEvent
if pasteEvent.clipboardData and pasteEvent.clipboardData.items if pasteEvent.clipboardData and pasteEvent.clipboardData.items
...@@ -162,7 +157,7 @@ class @DropzoneInput ...@@ -162,7 +157,7 @@ class @DropzoneInput
closeAlertMessage() closeAlertMessage()
success: (e, textStatus, response) -> success: (e, textStatus, response) ->
insertToTextArea(filename, formatLink(response.responseJSON.link)) insertToTextArea(filename, response.responseJSON.link.markdown)
error: (response) -> error: (response) ->
showError(response.responseJSON.message) showError(response.responseJSON.message)
...@@ -202,8 +197,3 @@ class @DropzoneInput ...@@ -202,8 +197,3 @@ class @DropzoneInput
e.preventDefault() e.preventDefault()
$(@).closest('.gfm-form').find('.div-dropzone').click() $(@).closest('.gfm-form').find('.div-dropzone').click()
return return
formatLink: (link) ->
text = "[#{link.alt}](#{link.url})"
text = "!#{text}" if link.is_image
text
...@@ -68,7 +68,7 @@ GitLab.GfmAutoComplete = ...@@ -68,7 +68,7 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.iid} #{m.title}" search: "#{m.iid} #{m.title}"
input.one 'focus', => if @dataSource
$.getJSON(@dataSource).done (data) -> $.getJSON(@dataSource).done (data) ->
# load members # load members
input.atwho 'load', '@', data.members input.atwho 'load', '@', data.members
......
#= require flash
#= require jquery.waitforimages #= require jquery.waitforimages
#= require task_list #= require task_list
...@@ -6,13 +7,44 @@ class @Issue ...@@ -6,13 +7,44 @@ class @Issue
# Prevent duplicate event bindings # Prevent duplicate event bindings
@disableTaskList() @disableTaskList()
if $("a.btn-close").length if $('a.btn-close').length
@initTaskList() @initTaskList()
@initIssueBtnEventListeners()
initTaskList: -> initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable') $('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
initIssueBtnEventListeners: ->
issueFailMessage = 'Unable to update this issue at this time.'
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
e.preventDefault()
e.stopImmediatePropagation()
$this = $(this)
isClose = $this.hasClass('btn-close')
$this.prop('disabled', true)
url = $this.attr('href')
$.ajax
type: 'PUT'
url: url,
error: (jqXHR, textStatus, errorThrown) ->
issueStatus = if isClose then 'close' else 'open'
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
if data.saved
$this.addClass('hidden')
if isClose
$('a.btn-reopen').removeClass('hidden')
$('div.status-box-closed').removeClass('hidden')
$('div.status-box-open').addClass('hidden')
else
$('a.btn-close').removeClass('hidden')
$('div.status-box-closed').addClass('hidden')
$('div.status-box-open').removeClass('hidden')
else
new Flash(issueFailMessage, 'alert')
$this.prop('disabled', false)
disableTaskList: -> disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable') $('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
......
...@@ -15,13 +15,6 @@ ...@@ -15,13 +15,6 @@
$(this).html totalIssues + 1 $(this).html totalIssues + 1
else else
$(this).html totalIssues - 1 $(this).html totalIssues - 1
$("body").on "click", ".issues-other-filters .dropdown-menu a", ->
$('.issues-list').block(
message: null,
overlayCSS:
backgroundColor: '#DDD'
opacity: .4
)
reload: -> reload: ->
Issues.initSelects() Issues.initSelects()
...@@ -54,7 +47,7 @@ ...@@ -54,7 +47,7 @@
form = $("#issue_search_form") form = $("#issue_search_form")
search = $("#issue_search").val() search = $("#issue_search").val()
$('.issues-holder').css("opacity", '0.5') $('.issues-holder').css("opacity", '0.5')
issues_url = form.attr('action') + '? '+ form.serialize() issues_url = form.attr('action') + '?' + form.serialize()
$.ajax $.ajax
type: "GET" type: "GET"
...@@ -65,7 +58,7 @@ ...@@ -65,7 +58,7 @@
success: (data) -> success: (data) ->
$('.issues-holder').html(data.html) $('.issues-holder').html(data.html)
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
History.replaceState {page: issues_url}, document.title, issues_url history.replaceState {page: issues_url}, document.title, issues_url
Issues.reload() Issues.reload()
dataType: "json" dataType: "json"
......
NProgress.configure(showSpinner: false)
defaultClass = 'tanuki-shape'
pieces = [
'path#tanuki-right-cheek',
'path#tanuki-right-eye, path#tanuki-right-ear',
'path#tanuki-nose',
'path#tanuki-left-eye, path#tanuki-left-ear',
'path#tanuki-left-cheek',
]
pieceIndex = 0
firstPiece = pieces[0]
currentTimer = null
delay = 150
clearHighlights = ->
$(".#{defaultClass}.highlight").attr('class', defaultClass)
start = ->
clearHighlights()
pieceIndex = 0
pieces.reverse() unless pieces[0] == firstPiece
clearInterval(currentTimer) if currentTimer
currentTimer = setInterval(work, delay)
stop = ->
clearInterval(currentTimer)
clearHighlights()
work = ->
clearHighlights()
$(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
# If we hit the last piece, reset the index and then reverse the array to
# get a nice back-and-forth sweeping look
if pieceIndex == pieces.length - 1
pieceIndex = 0
pieces.reverse()
else
pieceIndex++
$(document).on('page:fetch', start)
$(document).on('page:change', stop)
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
form = $("#issue_search_form") form = $("#issue_search_form")
search = $("#issue_search").val() search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5') $('.merge-requests-holder').css("opacity", '0.5')
issues_url = form.attr('action') + '? '+ form.serialize() issues_url = form.attr('action') + '?' + form.serialize()
$.ajax $.ajax
type: "GET" type: "GET"
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
success: (data) -> success: (data) ->
$('.merge-requests-holder').html(data.html) $('.merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
History.replaceState {page: issues_url}, document.title, issues_url history.replaceState {page: issues_url}, document.title, issues_url
MergeRequests.reload() MergeRequests.reload()
dataType: "json" dataType: "json"
......
class @ProjectFindFile
constructor: (@element, @options)->
@filePaths = {}
@inputElement = @element.find(".file-finder-input")
# init event
@initEvent()
# focus text input box
@inputElement.focus()
# load file list
@load(@options.url)
# init event
initEvent: ->
@inputElement.off "keyup"
@inputElement.on "keyup", (event) =>
target = $(event.target)
value = target.val()
oldValue = target.data("oldValue") ? ""
if value != oldValue
target.data("oldValue", value)
@findFile()
@element.find("tr.tree-item").eq(0).addClass("selected").focus()
@element.find(".tree-content-holder .tree-table").on "click", (event) ->
if (event.target.nodeName != "A")
path = @element.find(".tree-item-file-name a", this).attr("href")
location.href = path if path
# find file
findFile: ->
searchText = @inputElement.val()
result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
@renderList result, searchText
# files pathes load
load: (url) ->
$.ajax
url: url
method: "get"
dataType: "json"
success: (data) =>
@element.find(".loading").hide()
@filePaths = data
@findFile()
@element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
# render result
renderList: (filePaths, searchText) ->
@element.find(".tree-table > tbody").empty()
for filePath, i in filePaths
break if i == 20
if searchText
matches = fuzzaldrinPlus.match(filePath, searchText)
blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
html = @makeHtml filePath, matches, blobItemUrl
@element.find(".tree-table > tbody").append(html)
# highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
highlighter = (element, text, matches) ->
lastIndex = 0
highlightText = ""
matchedChars = []
for matchIndex in matches
unmatched = text.substring(lastIndex, matchIndex)
if unmatched
element.append(matchedChars.join("").bold()) if matchedChars.length
matchedChars = []
element.append(document.createTextNode(unmatched))
matchedChars.push(text[matchIndex])
lastIndex = matchIndex + 1
element.append(matchedChars.join("").bold()) if matchedChars.length
element.append(document.createTextNode(text.substring(lastIndex)))
# make tbody row html
makeHtml: (filePath, matches, blobItemUrl) ->
$tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
if matches
$tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
else
$tr.find("a").attr("href", blobItemUrl).text(filePath)
return $tr
selectRow: (type) ->
rows = @element.find(".files-slider tr.tree-item")
selectedRow = @element.find(".files-slider tr.tree-item.selected")
if rows && rows.length > 0
if selectedRow && selectedRow.length > 0
if type == "UP"
next = selectedRow.prev()
else if type == "DOWN"
next = selectedRow.next()
if next.length > 0
selectedRow.removeClass "selected"
selectedRow = next
else
selectedRow = rows.eq(0)
selectedRow.addClass("selected").focus()
selectRowUp: =>
@selectRow "UP"
selectRowDown: =>
@selectRow "DOWN"
goToTree: =>
location.href = @options.treeUrl
goToBlob: =>
path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
location.href = path if path
...@@ -17,8 +17,7 @@ class @Shortcuts ...@@ -17,8 +17,7 @@ class @Shortcuts
dataType: 'script', dataType: 'script',
success: (e) -> success: (e) ->
if location and location.length > 0 if location and location.length > 0
for l in location $(l).show() for l in location
$(l).show()
else else
$('.hidden-shortcut').show() $('.hidden-shortcut').show()
$('.js-more-help-button').remove() $('.js-more-help-button').remove()
...@@ -28,3 +27,8 @@ class @Shortcuts ...@@ -28,3 +27,8 @@ class @Shortcuts
@focusSearch: (e) -> @focusSearch: (e) ->
$('#search').focus() $('#search').focus()
e.preventDefault() e.preventDefault()
$(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
#= require shortcuts_navigation
class @ShortcutsFindFile extends ShortcutsNavigation
constructor: (@projectFindFile) ->
super()
_oldStopCallback = Mousetrap.stopCallback
# override to fire shortcuts action when focus in textbox
Mousetrap.stopCallback = (event, element, combo) =>
if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter')
# when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault()
return false
return _oldStopCallback(event, element, combo)
Mousetrap.bind('up', @projectFindFile.selectRowUp)
Mousetrap.bind('down', @projectFindFile.selectRowDown)
Mousetrap.bind('esc', @projectFindFile.goToTree)
Mousetrap.bind('enter', @projectFindFile.goToBlob)
class @ShortcutsTree extends ShortcutsNavigation
constructor: ->
super()
Mousetrap.bind('t', -> ShortcutsTree.findAndFollowLink('.shortcuts-find-file'))
...@@ -121,5 +121,5 @@ class @UsersSelect ...@@ -121,5 +121,5 @@ class @UsersSelect
callback(users) callback(users)
buildUrl: (url) -> buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root? url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root?
return url return url
# Zen Mode (full screen) textarea
#
#= provides zen_mode:enter
#= provides zen_mode:leave
#
#= require jquery.scrollTo
#= require dropzone #= require dropzone
#= require mousetrap #= require mousetrap
#= require mousetrap/pause #= require mousetrap/pause
#
# ### Events
#
# `zen_mode:enter`
#
# Fired when the "Edit in fullscreen" link is clicked.
#
# **Synchronicity** Sync
# **Bubbles** Yes
# **Cancelable** No
# **Target** a.js-zen-enter
#
# `zen_mode:leave`
#
# Fired when the "Leave Fullscreen" link is clicked.
#
# **Synchronicity** Sync
# **Bubbles** Yes
# **Cancelable** No
# **Target** a.js-zen-leave
#
class @ZenMode class @ZenMode
constructor: -> constructor: ->
@active_zen_area = null @active_backdrop = null
@active_checkbox = null @active_textarea = null
@scroll_position = 0
$(window).scroll => $(document).on 'click', '.js-zen-enter', (e) ->
if not @active_checkbox e.preventDefault()
@scroll_position = window.pageYOffset $(e.currentTarget).trigger('zen_mode:enter')
$('body').on 'click', '.zen-enter-link', (e) => $(document).on 'click', '.js-zen-leave', (e) ->
e.preventDefault() e.preventDefault()
$(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change() $(e.currentTarget).trigger('zen_mode:leave')
$(document).on 'zen_mode:enter', (e) =>
@enter(e.target.parentNode)
$(document).on 'zen_mode:leave', (e) =>
@exit()
$('body').on 'click', '.zen-leave-link', (e) => $(document).on 'keydown', (e) ->
if e.keyCode == 27 # Esc
e.preventDefault() e.preventDefault()
$(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change() $(document).trigger('zen_mode:leave')
$('body').on 'change', '.zen-toggle-comment', (e) => enter: (backdrop) ->
checkbox = e.currentTarget
if checkbox.checked
# Disable other keyboard shortcuts in ZEN mode
Mousetrap.pause() Mousetrap.pause()
@updateActiveZenArea(checkbox)
else
@exitZenMode()
$(document).on 'keydown', (e) => @active_backdrop = $(backdrop)
if e.keyCode is 27 # Esc @active_backdrop.addClass('fullscreen')
@exitZenMode()
e.preventDefault() @active_textarea = @active_backdrop.find('textarea')
updateActiveZenArea: (checkbox) =>
@active_checkbox = $(checkbox)
@active_checkbox.prop('checked', true)
@active_zen_area = @active_checkbox.parent().find('textarea')
# Prevent a user-resized textarea from persisting to fullscreen # Prevent a user-resized textarea from persisting to fullscreen
@active_zen_area.removeAttr('style') @active_textarea.removeAttr('style')
@active_zen_area.focus() @active_textarea.focus()
exitZenMode: => exit: ->
if @active_zen_area isnt null if @active_textarea
Mousetrap.unpause() Mousetrap.unpause()
@active_checkbox.prop('checked', false)
@active_zen_area = null @active_textarea.closest('.zen-backdrop').removeClass('fullscreen')
@active_checkbox = null
@restoreScroll(@scroll_position) @scrollTo(@active_textarea)
# Enable dropzone when leaving ZEN mode
@active_textarea = null
@active_backdrop = null
Dropzone.forElement('.div-dropzone').enable() Dropzone.forElement('.div-dropzone').enable()
restoreScroll: (y) -> scrollTo: (zen_area) ->
window.scrollTo(window.pageXOffset, y) $.scrollTo(zen_area, 0, offset: -150)
...@@ -72,6 +72,15 @@ ...@@ -72,6 +72,15 @@
> p:last-child { > p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.block-controls {
float: right;
.control {
float: left;
margin-left: 10px;
}
}
} }
.cover-block { .cover-block {
......
...@@ -19,38 +19,33 @@ ...@@ -19,38 +19,33 @@
} }
} }
} }
/** /**
* This overwrites the default values of the cal-heatmap gem * This overwrites the default values of the cal-heatmap gem
*/ */
.calendar { .calendar {
.qi { .qi {
background-color: #999;
fill: #fff; fill: #fff;
} }
.q1 { .q1 {
background-color: #dae289; fill: #ededed !important;
fill: #ededed;
} }
.q2 { .q2 {
background-color: #cedb9c; fill: #ACD5F2 !important;
fill: #ACD5F2;
} }
.q3 { .q3 {
background-color: #b5cf6b; fill: #7FA8D1 !important;
fill: #7FA8D1;
} }
.q4 { .q4 {
background-color: #637939; fill: #49729B !important;
fill: #49729B;
} }
.q5 { .q5 {
background-color: #3b6427; fill: #254E77 !important;
fill: #254E77;
} }
.domain-background { .domain-background {
...@@ -59,32 +54,7 @@ ...@@ -59,32 +54,7 @@
} }
.ch-tooltip { .ch-tooltip {
position: absolute;
display: none;
margin-top: 22px;
margin-left: 1px;
font-size: 13px;
padding: 3px; padding: 3px;
font-weight: 550; font-weight: 550;
background-color: #222;
span {
position: absolute;
width: 200px;
text-align: center;
visibility: hidden;
border-radius: 10px;
&:after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -8px;
width: 0;
height: 0;
border-top: 8px solid #000000;
border-right: 8px solid transparent;
border-left: 8px solid transparent;
}
}
} }
} }
...@@ -3,23 +3,23 @@ ...@@ -3,23 +3,23 @@
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf'); src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf.woff');
} }
@font-face { @font-face {
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf'); src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf.woff');
} }
@font-face { @font-face {
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf'); src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf.woff');
} }
@font-face { @font-face {
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf'); src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf.woff');
} }
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
.tanuki-shape { .tanuki-shape {
transition: all 0.8s; transition: all 0.8s;
&:hover { &:hover, &.highlight {
fill: rgb(255, 255, 255); fill: rgb(255, 255, 255);
transition: all 0.1s; transition: all 0.1s;
} }
......
...@@ -54,17 +54,17 @@ ...@@ -54,17 +54,17 @@
h3 { h3 {
margin: 24px 0 12px 0; margin: 24px 0 12px 0;
font-size: 1.25em; font-size: 1.1em;
} }
h4 { h4 {
margin: 24px 0 12px 0; margin: 24px 0 12px 0;
font-size: 1.1em; font-size: 0.98em;
} }
h5 { h5 {
margin: 24px 0 12px 0; margin: 24px 0 12px 0;
font-size: 1em; font-size: 0.95em;
} }
h6 { h6 {
......
.zennable { .zennable {
.zen-toggle-comment { a.js-zen-enter {
display: none;
}
.zen-enter-link {
color: $gl-gray; color: $gl-gray;
position: absolute; position: absolute;
top: 0px; top: 0px;
...@@ -11,7 +7,7 @@ ...@@ -11,7 +7,7 @@
line-height: 40px; line-height: 40px;
} }
.zen-leave-link { a.js-zen-leave {
display: none; display: none;
color: $gl-text-color; color: $gl-text-color;
position: absolute; position: absolute;
...@@ -25,19 +21,8 @@ ...@@ -25,19 +21,8 @@
} }
} }
// Hide the Enter link when we're in Zen mode .zen-backdrop {
input:checked ~ .zen-backdrop .zen-enter-link { &.fullscreen {
display: none;
}
// Show the Leave link when we're in Zen mode
input:checked ~ .zen-backdrop .zen-leave-link {
display: block;
position: absolute;
top: 0;
}
input:checked ~ .zen-backdrop {
background-color: white; background-color: white;
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -61,26 +46,16 @@ ...@@ -61,26 +46,16 @@
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
} }
}
// Make the color of the placeholder text in the Zenned-out textarea darker,
// so it becomes visible
input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { a.js-zen-enter {
color: #A8A8A8; display: none;
} }
input:checked ~ .zen-backdrop textarea:-moz-placeholder { a.js-zen-leave {
color: #A8A8A8; display: block;
opacity: 1; position: absolute;
top: 0;
} }
input:checked ~ .zen-backdrop textarea::-moz-placeholder {
color: #A8A8A8;
opacity: 1;
} }
input:checked ~ .zen-backdrop textarea:-ms-input-placeholder {
color: #A8A8A8;
} }
} }
...@@ -28,10 +28,6 @@ ...@@ -28,10 +28,6 @@
} }
} }
.commits-feed-holder {
float: right;
}
li.commit { li.commit {
list-style: none; list-style: none;
...@@ -122,3 +118,59 @@ li.commit { ...@@ -122,3 +118,59 @@ li.commit {
color: $gl-gray; color: $gl-gray;
} }
} }
.divergence-graph {
padding: 12px 12px 0 0;
float: right;
.graph-side {
position: relative;
width: 80px;
height: 22px;
padding: 5px 0 13px;
float: left;
.bar {
position: absolute;
height: 4px;
background-color: #ccc;
}
.bar-behind {
right: 0;
border-radius: 3px 0 0 3px;
}
.bar-ahead {
left: 0;
border-radius: 0 3px 3px 0;
}
.count {
padding-top: 6px;
padding-bottom: 0px;
font-size: 12px;
color: #333;
display: block;
}
.count-behind {
padding-right: 4px;
text-align: right;
}
.count-ahead {
padding-left: 4px;
text-align: left;
}
}
.graph-separator {
position: relative;
width: 1px;
height: 18px;
margin: 5px 0 0;
float: left;
background-color: #ccc;
}
}
...@@ -138,6 +138,7 @@ ...@@ -138,6 +138,7 @@
*/ */
.event-last-push { .event-last-push {
overflow: auto; overflow: auto;
width: 100%;
.event-last-push-text { .event-last-push-text {
@include str-truncated(100%); @include str-truncated(100%);
padding: 5px 0; padding: 5px 0;
......
...@@ -94,9 +94,17 @@ ...@@ -94,9 +94,17 @@
} }
.cross-project-reference { .cross-project-reference {
font-weight: bold;
color: $gl-link-color; color: $gl-link-color;
span {
white-space: nowrap;
width: 85%;
overflow: hidden;
position: relative;
display: inline-block;
text-overflow: ellipsis;
}
button { button {
float: right; float: right;
} }
......
...@@ -26,6 +26,13 @@ ...@@ -26,6 +26,13 @@
} }
.project-home-panel { .project-home-panel {
.cover-controls {
.project-settings-dropdown {
margin-left: 10px;
}
}
.project-identicon-holder { .project-identicon-holder {
margin-bottom: 16px; margin-bottom: 16px;
...@@ -292,10 +299,9 @@ ...@@ -292,10 +299,9 @@
border: 1px solid #c6cacf !important; border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important; background-color: #e4e7ed !important;
text-transform: uppercase; text-transform: none;
color: #313236 !important; color: #313236 !important;
font-size: 13px; font-size: 15px;
font-weight: 600;
} }
.dropdown-menu { .dropdown-menu {
...@@ -408,10 +414,15 @@ ul.nav.nav-projects-tabs { ...@@ -408,10 +414,15 @@ ul.nav.nav-projects-tabs {
} }
} }
.last-push-widget {
margin-top: -1px;
}
.top-area { .top-area {
border-bottom: 1px solid #EEE; border-bottom: 1px solid #EEE;
margin: 0 -16px; margin: 0 -16px;
padding: 0 $gl-padding; padding: 0 $gl-padding;
height: 42px;
ul.left-top-menu { ul.left-top-menu {
display: inline-block; display: inline-block;
...@@ -518,6 +529,7 @@ pre.light-well { ...@@ -518,6 +529,7 @@ pre.light-well {
.projects-search-form { .projects-search-form {
margin: -$gl-padding; margin: -$gl-padding;
padding: $gl-padding; padding: $gl-padding;
padding-bottom: 0;
margin-bottom: 0px; margin-bottom: 0px;
input { input {
......
.tree-holder { .tree-holder {
.file-finder {
width: 50%;
.file-finder-input {
width: 95%;
display: inline-block;
}
}
.tree-table { .tree-table {
margin-bottom: 0; margin-bottom: 0;
......
...@@ -9,12 +9,10 @@ class AbuseReportsController < ApplicationController ...@@ -9,12 +9,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user @abuse_report.reporter = current_user
if @abuse_report.save if @abuse_report.save
if current_application_settings.admin_notification_email.present? @abuse_report.notify
AbuseReportMailer.notify(@abuse_report.id).deliver_later
end
message = "Thank you for your report. A GitLab administrator will look into it shortly." message = "Thank you for your report. A GitLab administrator will look into it shortly."
redirect_to root_path, notice: message redirect_to @abuse_report.user, notice: message
else else
render :new render :new
end end
...@@ -23,6 +21,9 @@ class AbuseReportsController < ApplicationController ...@@ -23,6 +21,9 @@ class AbuseReportsController < ApplicationController
private private
def report_params def report_params
params.require(:abuse_report).permit(:user_id, :message) params.require(:abuse_report).permit(%i(
message
user_id
))
end end
end end
...@@ -69,6 +69,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -69,6 +69,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:shared_runners_enabled, :shared_runners_enabled,
:max_artifacts_size, :max_artifacts_size,
:max_pages_size, :max_pages_size,
:metrics_enabled,
:metrics_host,
:metrics_port,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
:recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key,
restricted_visibility_levels: [], restricted_visibility_levels: [],
import_sources: [] import_sources: []
) )
......
...@@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController ...@@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController
@builds = @all_builds.order('created_at DESC') @builds = @all_builds.order('created_at DESC')
@builds = @builds =
case @scope case @scope
when 'all' when 'running'
@builds @builds.running_or_pending.reverse_order
when 'finished' when 'finished'
@builds.finished @builds.finished
else else
@builds.running_or_pending.reverse_order @builds
end end
@builds = @builds.page(params[:page]).per(30) @builds = @builds.page(params[:page]).per(30)
end end
......
...@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base ...@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base
end end
def set_filters_params def set_filters_params
params[:sort] ||= 'created_desc' params[:sort] ||= 'id_desc'
params[:scope] = 'all' if params[:scope].blank? params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank? params[:state] = 'opened' if params[:state].blank?
......
...@@ -6,11 +6,13 @@ module Ci ...@@ -6,11 +6,13 @@ module Ci
end end
def create def create
if params[:content].blank? @content = params[:content]
if @content.blank?
@status = false @status = false
@error = "Please provide content of .gitlab-ci.yml" @error = "Please provide content of .gitlab-ci.yml"
else else
@config_processor = Ci::GitlabCiYamlProcessor.new params[:content] @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages @stages = @config_processor.stages
@builds = @config_processor.builds @builds = @config_processor.builds
@status = true @status = true
......
class Explore::GroupsController < Explore::ApplicationController class Explore::GroupsController < Explore::ApplicationController
def index def index
@groups = GroupsFinder.new.execute(current_user) @groups = Group.order_id_desc
@groups = @groups.search(params[:search]) if params[:search].present? @groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(PER_PAGE) @groups = @groups.page(params[:page]).per(PER_PAGE)
......
...@@ -9,6 +9,11 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -9,6 +9,11 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort] || 'name' @sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort) @branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
end end
def recent def recent
......
...@@ -12,12 +12,12 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -12,12 +12,12 @@ class Projects::BuildsController < Projects::ApplicationController
@builds = @all_builds.order('created_at DESC') @builds = @all_builds.order('created_at DESC')
@builds = @builds =
case @scope case @scope
when 'all' when 'running'
@builds @builds.running_or_pending.reverse_order
when 'finished' when 'finished'
@builds.finished @builds.finished
else else
@builds.running_or_pending.reverse_order @builds
end end
@builds = @builds.page(params[:page]).per(30) @builds = @builds.page(params[:page]).per(30)
end end
......
...@@ -8,10 +8,16 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -8,10 +8,16 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :authorize_download_code! before_action :authorize_download_code!
def show def show
@repo = @project.repository @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
@limit, @offset = (params[:limit] || 40), (params[:offset] || 0) search = params[:search]
@commits =
if search.present?
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact
else
@repository.commits(@ref, @path, @limit, @offset)
end
@commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)). @note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
......
# Controller for viewing a repository's file structure
class Projects::FindFileController < Projects::ApplicationController
include ExtractsPath
include ActionView::Helpers::SanitizeHelper
include TreeHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
def show
return render_404 unless @repository.commit(@ref)
respond_to do |format|
format.html
end
end
def list
file_paths = @repo.ls_files(@ref)
respond_to do |format|
format.json { render json: file_paths }
end
end
end
...@@ -159,7 +159,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -159,7 +159,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def merge_check def merge_check
@merge_request.check_if_can_be_merged if @merge_request.unchecked? @merge_request.check_if_can_be_merged
render partial: "projects/merge_requests/widget/show.html.haml", layout: false render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end end
......
...@@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_network_path(@project.namespace, @project, @id, @options) namespace_project_network_path(@project.namespace, @project, @id, @options)
when "graphs" when "graphs"
namespace_project_graph_path(@project.namespace, @project, @id) namespace_project_graph_path(@project.namespace, @project, @id)
when "find_file"
namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits" when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id) commits_namespace_project_graph_path(@project.namespace, @project, @id)
else else
......
...@@ -8,7 +8,7 @@ class ProjectsController < ApplicationController ...@@ -8,7 +8,7 @@ class ProjectsController < ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize # Authorize
before_action :authorize_admin_project!, only: [:edit, :update] before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
before_action :event_filter, only: [:show, :activity] before_action :event_filter, only: [:show, :activity]
layout :determine_layout layout :determine_layout
...@@ -177,6 +177,15 @@ class ProjectsController < ApplicationController ...@@ -177,6 +177,15 @@ class ProjectsController < ApplicationController
end end
end end
def housekeeping
::Projects::HousekeepingService.new(@project).execute
respond_to do |format|
flash[:notice] = "Housekeeping successfully started."
format.html { redirect_to project_path(@project) }
end
end
def toggle_star def toggle_star
current_user.toggle_star(@project) current_user.toggle_star(@project)
@project.reload @project.reload
...@@ -189,7 +198,7 @@ class ProjectsController < ApplicationController ...@@ -189,7 +198,7 @@ class ProjectsController < ApplicationController
def markdown_preview def markdown_preview
text = params[:text] text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user) ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text) ext.analyze(text)
render json: { render json: {
......
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled? before_action :signup_enabled?
include Recaptcha::Verify
def new def new
redirect_to(new_user_session_path) redirect_to(new_user_session_path)
end end
def create
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
flash.delete :recaptcha_error
render action: 'new'
end
end
def destroy def destroy
DeleteUserService.new(current_user).execute(current_user) DeleteUserService.new(current_user).execute(current_user)
...@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
def sign_up_params def sign_up_params
params.require(:user).permit(:username, :email, :name, :password, :password_confirmation) params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
end end
def resource_name
:user
end
def resource
@resource ||= User.new(sign_up_params)
end
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
end end
class SessionsController < Devise::SessionsController class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor include AuthenticatesWithTwoFactor
include Recaptcha::ClientHelper
prepend_before_action :authenticate_with_two_factor, only: [:create] prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new] prepend_before_action :store_redirect_path, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new] before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
def new def new
if Gitlab.config.ldap.enabled if Gitlab.config.ldap.enabled
...@@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController ...@@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController
AuditEventService.new(user, user, options). AuditEventService.new(user, user, options).
for_authentication.security_event for_authentication.security_event
end end
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
end end
...@@ -7,7 +7,7 @@ class UsersController < ApplicationController ...@@ -7,7 +7,7 @@ class UsersController < ApplicationController
@projects = PersonalProjectsFinder.new(@user).execute(current_user) @projects = PersonalProjectsFinder.new(@user).execute(current_user)
@groups = JoinedGroupsFinder.new(@user).execute(current_user) @groups = @user.groups.order_id_desc
respond_to do |format| respond_to do |format|
format.html format.html
......
class GroupsFinder
# Finds the groups available to the given user.
#
# current_user - The user to find the groups for.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end
private
# This method returns the groups "current_user" can see.
def groups_visible_to_user(current_user)
base = groups_for_projects(public_and_internal_projects)
union = Gitlab::SQL::Union.
new([base.select(:id), current_user.authorized_groups.select(:id)])
Group.where("namespaces.id IN (#{union.to_sql})")
end
def public_groups
groups_for_projects(public_projects)
end
def groups_for_projects(projects)
Group.public_and_given_groups(projects.select(:namespace_id))
end
def public_projects
Project.unscoped.public_only
end
def public_and_internal_projects
Project.unscoped.public_and_internal_only
end
end
...@@ -80,9 +80,9 @@ class IssuableFinder ...@@ -80,9 +80,9 @@ class IssuableFinder
if project? if project?
@projects = project @projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related? elsif current_user && params[:authorized_only].presence && !current_user_related?
@projects = current_user.authorized_projects @projects = current_user.authorized_projects.reorder(nil)
else else
@projects = ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end end
end end
......
# Class for finding the groups a user is a member of.
class JoinedGroupsFinder
def initialize(user = nil)
@user = user
end
# Finds the groups of the source user, optionally limited to those visible to
# the current user.
#
# current_user - If given the groups of "@user" will only include the groups
# "current_user" can also see.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end
private
# Returns the groups the user in "current_user" can see.
#
# This list includes all public/internal projects as well as the projects of
# "@user" that "current_user" also has access to.
def groups_visible_to_user(current_user)
base = @user.authorized_groups.visible_to_user(current_user)
extra = public_and_internal_groups
union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
Group.where("namespaces.id IN (#{union.to_sql})")
end
def public_groups
groups_for_projects(@user.authorized_projects.public_only)
end
def public_and_internal_groups
groups_for_projects(@user.authorized_projects.public_and_internal_only)
end
def groups_for_projects(projects)
@user.groups.public_and_given_groups(projects.select(:namespace_id))
end
end
...@@ -72,7 +72,7 @@ module ApplicationHelper ...@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user = user_or_email user = user_or_email
else else
user = User.find_by(email: user_or_email) user = User.find_by(email: user_or_email.downcase)
end end
if user if user
...@@ -206,7 +206,7 @@ module ApplicationHelper ...@@ -206,7 +206,7 @@ module ApplicationHelper
element = content_tag :time, time.to_s, element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago js-timeago-pending", class: "#{html_class} js-timeago js-timeago-pending",
datetime: time.to_time.getutc.iso8601, datetime: time.to_time.getutc.iso8601,
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), title: time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' } data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js unless skip_js
......
module AuthHelper module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
def ldap_enabled? def ldap_enabled?
......
...@@ -69,6 +69,10 @@ module IssuesHelper ...@@ -69,6 +69,10 @@ module IssuesHelper
end end
end end
def issue_button_visibility(issue, closed)
return 'hidden' if issue.closed? == closed
end
def issue_to_atom(xml, issue) def issue_to_atom(xml, issue)
xml.entry do xml.entry do
xml.id namespace_project_issue_url(issue.project.namespace, xml.id namespace_project_issue_url(issue.project.namespace,
...@@ -76,7 +80,7 @@ module IssuesHelper ...@@ -76,7 +80,7 @@ module IssuesHelper
xml.link href: namespace_project_issue_url(issue.project.namespace, xml.link href: namespace_project_issue_url(issue.project.namespace,
issue.project, issue) issue.project, issue)
xml.title truncate(issue.title, length: 80) xml.title truncate(issue.title, length: 80)
xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") xml.updated issue.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do |author| xml.author do |author|
xml.name issue.author_name xml.name issue.author_name
...@@ -95,13 +99,16 @@ module IssuesHelper ...@@ -95,13 +99,16 @@ module IssuesHelper
end end
def emoji_icon(name, unicode = nil, aliases = []) def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name) unicode ||= Emoji.emoji_filename(name) rescue ""
content_tag :div, "", content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}", class: "icon emoji-icon emoji-#{unicode}",
"data-emoji" => name, title: name,
"data-aliases" => aliases.join(" "), data: {
"data-unicode-name" => unicode aliases: aliases.join(' '),
emoji: name,
unicode_name: unicode
}
end end
def emoji_author_list(notes, current_user) def emoji_author_list(notes, current_user)
......
...@@ -67,7 +67,7 @@ module NotesHelper ...@@ -67,7 +67,7 @@ module NotesHelper
line_type: line_type line_type: line_type
} }
button_tag class: 'btn reply-btn js-discussion-reply-button', button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button',
data: data, title: 'Add a reply' do data: data, title: 'Add a reply' do
link_text = icon('comment') link_text = icon('comment')
link_text << ' Reply' link_text << ' Reply'
......
...@@ -27,35 +27,20 @@ module PageLayoutHelper ...@@ -27,35 +27,20 @@ module PageLayoutHelper
# #
# Returns an HTML-safe String. # Returns an HTML-safe String.
def page_description(description = nil) def page_description(description = nil)
@page_description ||= page_description_default
if description.present? if description.present?
@page_description = description.squish @page_description = description.squish
else elsif @page_description.present?
sanitize(@page_description, tags: []).truncate_words(30) sanitize(@page_description, tags: []).truncate_words(30)
end end
end end
# Default value for page_description when one hasn't been defined manually by
# a view
def page_description_default
if @project
@project.description || brand_title
else
brand_title
end
end
def page_image def page_image
default = image_url('gitlab_logo.png') default = image_url('gitlab_logo.png')
if @project subject = @project || @user || @group
@project.avatar_url || default
elsif @user image = subject.avatar_url if subject.present?
avatar_icon(@user) image || default
else
default
end
end end
# Define or get attributes to be used as Twitter card metadata # Define or get attributes to be used as Twitter card metadata
......
...@@ -70,7 +70,7 @@ module SearchHelper ...@@ -70,7 +70,7 @@ module SearchHelper
# Autocomplete results for the current user's groups # Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5) def groups_autocomplete(term, limit = 5)
GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group| Group.search(term).limit(limit).map do |group|
{ {
label: "group: #{search_result_sanitize(group.name)}", label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group) url: group_path(group)
......
...@@ -19,7 +19,7 @@ module SortingHelper ...@@ -19,7 +19,7 @@ module SortingHelper
end end
def sort_title_recently_updated def sort_title_recently_updated
'Recently updated' 'Last updated'
end end
def sort_title_oldest_created def sort_title_oldest_created
...@@ -27,7 +27,7 @@ module SortingHelper ...@@ -27,7 +27,7 @@ module SortingHelper
end end
def sort_title_recently_created def sort_title_recently_created
'Recently created' 'Last created'
end end
def sort_title_milestone_soon def sort_title_milestone_soon
...@@ -63,11 +63,11 @@ module SortingHelper ...@@ -63,11 +63,11 @@ module SortingHelper
end end
def sort_value_oldest_created def sort_value_oldest_created
'created_asc' 'id_asc'
end end
def sort_value_recently_created def sort_value_recently_created
'created_desc' 'id_desc'
end end
def sort_value_milestone_soon def sort_value_milestone_soon
......
...@@ -2,6 +2,8 @@ class AbuseReportMailer < BaseMailer ...@@ -2,6 +2,8 @@ class AbuseReportMailer < BaseMailer
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
def notify(abuse_report_id) def notify(abuse_report_id)
return unless deliverable?
@abuse_report = AbuseReport.find(abuse_report_id) @abuse_report = AbuseReport.find(abuse_report_id)
mail( mail(
...@@ -9,4 +11,10 @@ class AbuseReportMailer < BaseMailer ...@@ -9,4 +11,10 @@ class AbuseReportMailer < BaseMailer
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse" subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
) )
end end
private
def deliverable?
current_application_settings.admin_notification_email.present?
end
end end
...@@ -48,7 +48,7 @@ module Emails ...@@ -48,7 +48,7 @@ module Emails
yield yield
SentNotification.record(@note, recipient_id, reply_key) SentNotification.record_note(@note, recipient_id, reply_key)
end end
end end
end end
...@@ -85,7 +85,7 @@ class Ability ...@@ -85,7 +85,7 @@ class Ability
subject.group subject.group
end end
if group && group.public_profile? if group && group.projects.public_only.any?
[:read_group] [:read_group]
else else
[] []
......
...@@ -18,4 +18,10 @@ class AbuseReport < ActiveRecord::Base ...@@ -18,4 +18,10 @@ class AbuseReport < ActiveRecord::Base
validates :user, presence: true validates :user, presence: true
validates :message, presence: true validates :message, presence: true
validates :user_id, uniqueness: true validates :user_id, uniqueness: true
def notify
return unless self.persisted?
AbuseReportMailer.notify(self.id).deliver_later
end
end end
...@@ -28,9 +28,20 @@ ...@@ -28,9 +28,20 @@
# admin_notification_email :string(255) # admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null # shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null # max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255) # runners_registration_token :string
# require_two_factor_authentication :boolean default(TRUE) # require_two_factor_authentication :boolean default(FALSE)
# two_factor_grace_period :integer default(48) # two_factor_grace_period :integer default(48)
# metrics_enabled :boolean default(FALSE)
# metrics_host :string default("localhost")
# metrics_username :string
# metrics_password :string
# metrics_pool_size :integer default(16)
# metrics_timeout :integer default(10)
# metrics_method_call_threshold :integer default(10)
# recaptcha_enabled :boolean default(FALSE)
# recaptcha_site_key :string
# recaptcha_private_key :string
# metrics_port :integer default(8089)
# #
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
...@@ -64,6 +75,14 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -64,6 +75,14 @@ class ApplicationSetting < ActiveRecord::Base
validates :two_factor_grace_period, validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :recaptcha_site_key,
presence: true,
if: :recaptcha_enabled
validates :recaptcha_private_key,
presence: true,
if: :recaptcha_enabled
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil? unless value.nil?
value.each do |level| value.each do |level|
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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