Merge branch 'master' into ci/api-builds
* master: (108 commits) Fix project transfer e-mail sending incorrect paths in e-mail notification Update CHANGELOG Use Gitlab::CurrentSettings for InfluxDB Write to InfluxDB directly via UDP Strip newlines from obfuscated SQL Add hotfix that allows to access build artifacts created before 8.3 note votes methids implementation When reCAPTCHA is disabled, allow registrations to go through without a code Downcased user or email search for avatar_icon. Handle missing settings table for metrics Fix broken link in permissions page [ci skip] reCAPTCHA is configurable through Admin Settings, no reload needed. Fixed syntax in gitlab.yml.example Move InfluxDB settings to ApplicationSetting Fix spelling mistake, thanks Connor. Restart settings are moved too. Hotfix for builds trace data integrity add issue weight to contributing Added host option for InfluxDB Fixed styling of MetricsWorker specs ...
This commit is contained in:
commit
44dd4782f0
|
@ -12,6 +12,7 @@ before_script:
|
|||
|
||||
spec:feature:
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
|
||||
tags:
|
||||
- ruby
|
||||
|
|
23
CHANGELOG
23
CHANGELOG
|
@ -1,21 +1,35 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.4.0 (unreleased)
|
||||
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
|
||||
- Implement new UI for group page
|
||||
- Implement search inside emoji picker
|
||||
- Add API support for looking up a user by username (Stan Hu)
|
||||
- Add project permissions to all project API endpoints (Stan Hu)
|
||||
- Only allow group/project members to mention `@all`
|
||||
- Expose Git's version in the admin area
|
||||
- Add "Frequently used" category to emoji picker
|
||||
- Add CAS support (tduehr)
|
||||
- Add link to merge request on build detail page
|
||||
- Revert back upvote and downvote button to the issue and MR pages
|
||||
- Enable "Add key" button when user fills in a proper key (Stan Hu)
|
||||
|
||||
v 8.3.3 (unreleased)
|
||||
- Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
|
||||
|
||||
v 8.3.1 (unreleased)
|
||||
v 8.3.2
|
||||
- Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
|
||||
- Add support for Google reCAPTCHA in user registration
|
||||
|
||||
v 8.3.1
|
||||
- Fix Error 500 when global milestones have slashes (Stan Hu)
|
||||
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
|
||||
- Fix LDAP identity and user retrieval when special characters are used
|
||||
- Move Sidekiq-cron configuration to gitlab.yml
|
||||
- Enable forcing Two-Factor authentication sitewide, with optional grace period
|
||||
|
||||
v 8.3.0
|
||||
- Add CAS support (tduehr)
|
||||
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
|
||||
- API support for starred projects for authorized user (Zeger-Jan van de Weg)
|
||||
- Add link to merge request on build detail page.
|
||||
- Add open_issues_count to project API (Stan Hu)
|
||||
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
|
||||
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
|
||||
|
@ -75,7 +89,6 @@ v 8.3.0
|
|||
- Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
|
||||
- Persist runners registration token in database
|
||||
- Fix online editor should not remove newlines at the end of the file
|
||||
- Expose Git's version in the admin area
|
||||
|
||||
v 8.2.3
|
||||
- Fix application settings cache not expiring after changes (Stan Hu)
|
||||
|
|
|
@ -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
|
||||
|
||||
We welcome merge requests with fixes and improvements to GitLab code, tests,
|
||||
|
|
13
Gemfile
13
Gemfile
|
@ -35,6 +35,9 @@ gem 'omniauth-twitter', '~> 1.2.0'
|
|||
gem 'omniauth_crowd'
|
||||
gem 'rack-oauth2', '~> 1.2.1'
|
||||
|
||||
# reCAPTCHA protection
|
||||
gem 'recaptcha', require: 'recaptcha/rails'
|
||||
|
||||
# Two-factor authentication
|
||||
gem 'devise-two-factor', '~> 2.0.0'
|
||||
gem 'rqrcode-rails3', '~> 0.1.7'
|
||||
|
@ -212,9 +215,17 @@ gem 'select2-rails', '~> 3.5.9'
|
|||
gem 'virtus', '~> 1.0.1'
|
||||
gem 'net-ssh', '~> 3.0.1'
|
||||
|
||||
# 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
|
||||
gem "foreman"
|
||||
gem 'brakeman', '3.0.1', require: false
|
||||
gem 'brakeman', '~> 3.1.0', require: false
|
||||
|
||||
gem "annotate", "~> 2.6.0"
|
||||
gem "letter_opener", '~> 1.1.2'
|
||||
|
|
56
Gemfile.lock
56
Gemfile.lock
|
@ -49,6 +49,7 @@ GEM
|
|||
addressable (2.3.8)
|
||||
after_commit_queue (1.3.0)
|
||||
activerecord (>= 3.0)
|
||||
allocations (1.0.1)
|
||||
annotate (2.6.10)
|
||||
activerecord (>= 3.2, <= 4.3)
|
||||
rake (~> 10.4)
|
||||
|
@ -65,7 +66,7 @@ GEM
|
|||
attr_encrypted (1.3.4)
|
||||
encryptor (>= 1.3.0)
|
||||
attr_required (1.0.0)
|
||||
autoprefixer-rails (6.1.1)
|
||||
autoprefixer-rails (6.1.2)
|
||||
execjs
|
||||
json
|
||||
awesome_print (1.2.0)
|
||||
|
@ -84,15 +85,17 @@ GEM
|
|||
bootstrap-sass (3.3.5)
|
||||
autoprefixer-rails (>= 5.0.0.1)
|
||||
sass (>= 3.2.19)
|
||||
brakeman (3.0.1)
|
||||
brakeman (3.1.4)
|
||||
erubis (~> 2.6)
|
||||
fastercsv (~> 1.5)
|
||||
haml (>= 3.0, < 5.0)
|
||||
highline (~> 1.6.20)
|
||||
highline (>= 1.6.20, < 2.0)
|
||||
multi_json (~> 1.2)
|
||||
ruby2ruby (~> 2.1.1)
|
||||
ruby_parser (~> 3.5.0)
|
||||
ruby2ruby (>= 2.1.1, < 2.3.0)
|
||||
ruby_parser (~> 3.7.0)
|
||||
safe_yaml (>= 1.0)
|
||||
sass (~> 3.0)
|
||||
slim (>= 1.3.6, < 4.0)
|
||||
terminal-table (~> 1.4)
|
||||
browser (1.0.1)
|
||||
builder (3.2.2)
|
||||
|
@ -102,7 +105,7 @@ GEM
|
|||
bundler-audit (0.4.0)
|
||||
bundler (~> 1.2)
|
||||
thor (~> 0.18)
|
||||
byebug (8.2.0)
|
||||
byebug (8.2.1)
|
||||
cal-heatmap-rails (0.0.1)
|
||||
capybara (2.4.4)
|
||||
mime-types (>= 1.16)
|
||||
|
@ -117,6 +120,7 @@ GEM
|
|||
activemodel (>= 3.2.0)
|
||||
activesupport (>= 3.2.0)
|
||||
json (>= 1.7)
|
||||
cause (0.1)
|
||||
charlock_holmes (0.7.3)
|
||||
chunky_png (1.3.5)
|
||||
cliver (0.3.2)
|
||||
|
@ -140,10 +144,10 @@ GEM
|
|||
term-ansicolor (~> 1.3)
|
||||
thor (~> 0.19.1)
|
||||
tins (~> 1.6.0)
|
||||
crack (0.4.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
creole (0.5.0)
|
||||
d3_rails (3.5.6)
|
||||
d3_rails (3.5.11)
|
||||
railties (>= 3.1.0)
|
||||
daemons (1.2.3)
|
||||
database_cleaner (1.4.1)
|
||||
|
@ -230,7 +234,7 @@ GEM
|
|||
ipaddress (~> 0.5)
|
||||
nokogiri (~> 1.5, >= 1.5.11)
|
||||
opennebula
|
||||
fog-brightbox (0.9.0)
|
||||
fog-brightbox (0.10.1)
|
||||
fog-core (~> 1.22)
|
||||
fog-json
|
||||
inflecto (~> 0.0.2)
|
||||
|
@ -249,7 +253,7 @@ GEM
|
|||
fog-core (>= 1.21.0)
|
||||
fog-json
|
||||
fog-xml (>= 0.0.1)
|
||||
fog-sakuracloud (1.4.0)
|
||||
fog-sakuracloud (1.5.0)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-softlayer (1.0.2)
|
||||
|
@ -277,11 +281,11 @@ GEM
|
|||
ruby-progressbar (~> 1.4)
|
||||
gemnasium-gitlab-service (0.2.6)
|
||||
rugged (~> 0.21)
|
||||
gemojione (2.1.0)
|
||||
gemojione (2.1.1)
|
||||
json
|
||||
get_process_mem (0.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
github-linguist (4.7.2)
|
||||
github-linguist (4.7.3)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
escape_utils (~> 1.1.0)
|
||||
mime-types (>= 1.19)
|
||||
|
@ -298,7 +302,7 @@ GEM
|
|||
posix-spawn (~> 0.3)
|
||||
gitlab_emoji (0.2.0)
|
||||
gemojione (~> 2.1)
|
||||
gitlab_git (7.2.21)
|
||||
gitlab_git (7.2.22)
|
||||
activesupport (~> 4.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
github-linguist (~> 4.7.0)
|
||||
|
@ -347,7 +351,7 @@ GEM
|
|||
html2haml (>= 1.0.1)
|
||||
railties (>= 4.0.1)
|
||||
hashie (3.4.3)
|
||||
highline (1.6.21)
|
||||
highline (1.7.8)
|
||||
hike (1.2.3)
|
||||
hipchat (1.5.2)
|
||||
httparty
|
||||
|
@ -370,6 +374,9 @@ GEM
|
|||
i18n (0.7.0)
|
||||
ice_nine (0.11.1)
|
||||
inflecto (0.0.2)
|
||||
influxdb (0.2.3)
|
||||
cause
|
||||
json
|
||||
ipaddress (0.8.0)
|
||||
jquery-atwho-rails (1.3.2)
|
||||
jquery-rails (4.0.5)
|
||||
|
@ -417,7 +424,7 @@ GEM
|
|||
net-ldap (0.12.1)
|
||||
net-ssh (3.0.1)
|
||||
netrc (0.11.0)
|
||||
newrelic-grape (2.0.0)
|
||||
newrelic-grape (2.1.0)
|
||||
grape
|
||||
newrelic_rpm
|
||||
newrelic_rpm (3.9.4.245)
|
||||
|
@ -566,6 +573,8 @@ GEM
|
|||
trollop
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
recaptcha (1.0.2)
|
||||
json
|
||||
redcarpet (3.3.3)
|
||||
redis (3.2.2)
|
||||
redis-actionpack (4.0.1)
|
||||
|
@ -636,10 +645,10 @@ GEM
|
|||
ruby-saml (1.0.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
uuid (~> 2.3)
|
||||
ruby2ruby (2.1.4)
|
||||
ruby2ruby (2.2.0)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.0)
|
||||
ruby_parser (3.5.0)
|
||||
ruby_parser (3.7.2)
|
||||
sexp_processor (~> 4.1)
|
||||
rubyntlm (0.5.2)
|
||||
rubypants (0.2.0)
|
||||
|
@ -693,6 +702,9 @@ GEM
|
|||
tilt (>= 1.3, < 3)
|
||||
six (0.2.0)
|
||||
slack-notifier (1.2.1)
|
||||
slim (3.0.6)
|
||||
temple (~> 0.7.3)
|
||||
tilt (>= 1.3.3, < 2.1)
|
||||
slop (3.6.0)
|
||||
spinach (0.8.10)
|
||||
colorize
|
||||
|
@ -734,6 +746,7 @@ GEM
|
|||
railties (>= 3.2.5, < 5)
|
||||
teaspoon-jasmine (2.2.0)
|
||||
teaspoon (>= 1.0.0)
|
||||
temple (0.7.6)
|
||||
term-ansicolor (1.3.2)
|
||||
tins (~> 1.0)
|
||||
terminal-table (1.5.2)
|
||||
|
@ -789,7 +802,7 @@ GEM
|
|||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.3)
|
||||
warden (1.2.4)
|
||||
rack (>= 1.0)
|
||||
web-console (2.2.1)
|
||||
activemodel (>= 4.0)
|
||||
|
@ -820,6 +833,7 @@ DEPENDENCIES
|
|||
acts-as-taggable-on (~> 3.4)
|
||||
addressable (~> 2.3.8)
|
||||
after_commit_queue
|
||||
allocations (~> 1.0)
|
||||
annotate (~> 2.6.0)
|
||||
asana (~> 0.4.0)
|
||||
asciidoctor (~> 1.5.2)
|
||||
|
@ -830,7 +844,7 @@ DEPENDENCIES
|
|||
better_errors (~> 1.0.1)
|
||||
binding_of_caller (~> 0.7.2)
|
||||
bootstrap-sass (~> 3.0)
|
||||
brakeman (= 3.0.1)
|
||||
brakeman (~> 3.1.0)
|
||||
browser (~> 1.0.0)
|
||||
bullet
|
||||
bundler-audit
|
||||
|
@ -842,6 +856,7 @@ DEPENDENCIES
|
|||
charlock_holmes (~> 0.7.3)
|
||||
coffee-rails (~> 4.1.0)
|
||||
colorize (~> 0.7.0)
|
||||
connection_pool (~> 2.0)
|
||||
coveralls (~> 0.8.2)
|
||||
creole (~> 0.5.0)
|
||||
d3_rails (~> 3.5.5)
|
||||
|
@ -879,6 +894,7 @@ DEPENDENCIES
|
|||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 1.11.0)
|
||||
httparty (~> 0.13.3)
|
||||
influxdb (~> 0.2)
|
||||
jquery-atwho-rails (~> 1.3.2)
|
||||
jquery-rails (~> 4.0.0)
|
||||
jquery-scrollto-rails (~> 1.4.3)
|
||||
|
@ -887,6 +903,7 @@ DEPENDENCIES
|
|||
kaminari (~> 0.16.3)
|
||||
letter_opener (~> 1.1.2)
|
||||
mail_room (~> 0.6.1)
|
||||
method_source (~> 0.8)
|
||||
minitest (~> 5.7.0)
|
||||
mousetrap-rails (~> 1.4.6)
|
||||
mysql2 (~> 0.3.16)
|
||||
|
@ -924,6 +941,7 @@ DEPENDENCIES
|
|||
raphael-rails (~> 2.1.2)
|
||||
rblineprof
|
||||
rdoc (~> 3.6)
|
||||
recaptcha
|
||||
redcarpet (~> 3.3.3)
|
||||
redis-namespace
|
||||
redis-rails (~> 4.0.0)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
|
@ -43,15 +43,19 @@ class @AwardsHandler
|
|||
|
||||
decrementCounter: (emoji) ->
|
||||
counter = @findEmojiIcon(emoji).siblings(".counter")
|
||||
emojiIcon = counter.parent()
|
||||
|
||||
if parseInt(counter.text()) > 1
|
||||
counter.text(parseInt(counter.text()) - 1)
|
||||
counter.parent().removeClass("active")
|
||||
emojiIcon.removeClass("active")
|
||||
@removeMeFromAuthorList(emoji)
|
||||
else if emoji =="thumbsup" || emoji == "thumbsdown"
|
||||
emojiIcon.tooltip("destroy")
|
||||
counter.text(0)
|
||||
emojiIcon.removeClass("active")
|
||||
else
|
||||
award = counter.parent()
|
||||
award.tooltip("destroy")
|
||||
award.remove()
|
||||
emojiIcon.tooltip("destroy")
|
||||
emojiIcon.remove()
|
||||
|
||||
removeMeFromAuthorList: (emoji) ->
|
||||
award_block = @findEmojiIcon(emoji).parent()
|
||||
|
@ -127,21 +131,19 @@ class @AwardsHandler
|
|||
|
||||
getFrequentlyUsedEmojis: ->
|
||||
frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",")
|
||||
|
||||
frequently_used_emojis = ["thumbsup", "thumbsdown"].concat(frequently_used_emojis)
|
||||
|
||||
_.compact(_.uniq(frequently_used_emojis))
|
||||
|
||||
renderFrequentlyUsedBlock: ->
|
||||
frequently_used_emojis = @getFrequentlyUsedEmojis()
|
||||
if $.cookie('frequently_used_emojis')
|
||||
frequently_used_emojis = @getFrequentlyUsedEmojis()
|
||||
|
||||
ul = $("<ul>")
|
||||
ul = $("<ul>")
|
||||
|
||||
for emoji in frequently_used_emojis
|
||||
do (emoji) ->
|
||||
$(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
|
||||
for emoji in frequently_used_emojis
|
||||
do (emoji) ->
|
||||
$(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
|
||||
|
||||
$("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
|
||||
$("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
|
||||
|
||||
setupSearch: ->
|
||||
$("input.emoji-search").keyup (ev) =>
|
||||
|
|
|
@ -49,7 +49,7 @@ class Dispatcher
|
|||
new DropzoneInput($('.release-form'))
|
||||
when 'projects:merge_requests:show'
|
||||
new Diff()
|
||||
shortcut_handler = new ShortcutsIssuable()
|
||||
shortcut_handler = new ShortcutsIssuable(true)
|
||||
new ZenMode()
|
||||
when "projects:merge_requests:diffs"
|
||||
new Diff()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#= require flash
|
||||
#= require jquery.waitforimages
|
||||
#= require task_list
|
||||
|
||||
|
@ -6,13 +7,44 @@ class @Issue
|
|||
# Prevent duplicate event bindings
|
||||
@disableTaskList()
|
||||
|
||||
if $("a.btn-close").length
|
||||
if $('a.btn-close').length
|
||||
@initTaskList()
|
||||
@initIssueBtnEventListeners()
|
||||
|
||||
initTaskList: ->
|
||||
$('.detail-page-description .js-task-list-container').taskList('enable')
|
||||
$(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: ->
|
||||
$('.detail-page-description .js-task-list-container').taskList('disable')
|
||||
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
|
||||
|
|
|
@ -7,7 +7,7 @@ class @Shortcuts
|
|||
|
||||
selectiveHelp: (e) =>
|
||||
Shortcuts.showHelp(e, @enabledHelp)
|
||||
|
||||
|
||||
@showHelp: (e, location) ->
|
||||
if $('#modal-shortcuts').length > 0
|
||||
$('#modal-shortcuts').modal('show')
|
||||
|
@ -17,8 +17,7 @@ class @Shortcuts
|
|||
dataType: 'script',
|
||||
success: (e) ->
|
||||
if location and location.length > 0
|
||||
for l in location
|
||||
$(l).show()
|
||||
$(l).show() for l in location
|
||||
else
|
||||
$('.hidden-shortcut').show()
|
||||
$('.js-more-help-button').remove()
|
||||
|
@ -28,3 +27,8 @@ class @Shortcuts
|
|||
@focusSearch: (e) ->
|
||||
$('#search').focus()
|
||||
e.preventDefault()
|
||||
|
||||
$(document).on 'click.more_help', '.js-more-help-button', (e) ->
|
||||
$(@).remove()
|
||||
$('.hidden-shortcut').show()
|
||||
e.preventDefault()
|
||||
|
|
|
@ -117,5 +117,5 @@ class @UsersSelect
|
|||
callback(users)
|
||||
|
||||
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
|
||||
|
|
|
@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
:default_branch_protection,
|
||||
:signup_enabled,
|
||||
:signin_enabled,
|
||||
:require_two_factor_authentication,
|
||||
:two_factor_grace_period,
|
||||
:gravatar_enabled,
|
||||
:twitter_sharing_enabled,
|
||||
:sign_in_text,
|
||||
|
@ -65,6 +67,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
:user_oauth_applications,
|
||||
:shared_runners_enabled,
|
||||
:max_artifacts_size,
|
||||
:metrics_enabled,
|
||||
:metrics_host,
|
||||
:metrics_port,
|
||||
:metrics_username,
|
||||
:metrics_password,
|
||||
:metrics_pool_size,
|
||||
:metrics_timeout,
|
||||
:metrics_method_call_threshold,
|
||||
:recaptcha_enabled,
|
||||
:recaptcha_site_key,
|
||||
:recaptcha_private_key,
|
||||
restricted_visibility_levels: [],
|
||||
import_sources: []
|
||||
)
|
||||
|
|
|
@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
|
|||
before_action :validate_user_service_ticket!
|
||||
before_action :reject_blocked!
|
||||
before_action :check_password_expiration
|
||||
before_action :check_2fa_requirement
|
||||
before_action :ldap_security_check
|
||||
before_action :default_headers
|
||||
before_action :add_gon_variables
|
||||
|
@ -223,6 +224,12 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def check_2fa_requirement
|
||||
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
|
||||
redirect_to new_profile_two_factor_auth_path
|
||||
end
|
||||
end
|
||||
|
||||
def ldap_security_check
|
||||
if current_user && current_user.requires_ldap_check?
|
||||
unless Gitlab::LDAP::Access.allowed?(current_user)
|
||||
|
@ -357,6 +364,23 @@ class ApplicationController < ActionController::Base
|
|||
current_application_settings.import_sources.include?('git')
|
||||
end
|
||||
|
||||
def two_factor_authentication_required?
|
||||
current_application_settings.require_two_factor_authentication
|
||||
end
|
||||
|
||||
def two_factor_grace_period
|
||||
current_application_settings.two_factor_grace_period
|
||||
end
|
||||
|
||||
def two_factor_grace_period_expired?
|
||||
date = current_user.otp_grace_period_started_at
|
||||
date && (date + two_factor_grace_period.hours) < Time.current
|
||||
end
|
||||
|
||||
def skip_two_factor?
|
||||
session[:skip_tfa] && session[:skip_tfa] > Time.current
|
||||
end
|
||||
|
||||
def redirect_to_home_page_url?
|
||||
# If user is not signed-in and tries to access root_path - redirect him to landing page
|
||||
# Don't redirect to the default URL to prevent endless redirections
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
skip_before_action :check_2fa_requirement
|
||||
|
||||
def new
|
||||
unless current_user.otp_secret
|
||||
current_user.otp_secret = User.generate_otp_secret(32)
|
||||
current_user.save!
|
||||
end
|
||||
|
||||
unless current_user.otp_grace_period_started_at && two_factor_grace_period
|
||||
current_user.otp_grace_period_started_at = Time.current
|
||||
end
|
||||
|
||||
current_user.save! if current_user.changed?
|
||||
|
||||
if two_factor_grace_period_expired?
|
||||
flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
|
||||
else
|
||||
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
|
||||
flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
|
||||
end
|
||||
|
||||
@qr_code = build_qr_code
|
||||
|
@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
redirect_to profile_account_path
|
||||
end
|
||||
|
||||
def skip
|
||||
if two_factor_grace_period_expired?
|
||||
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
|
||||
else
|
||||
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_qr_code
|
||||
|
|
|
@ -9,7 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
|
||||
def show
|
||||
@repo = @project.repository
|
||||
@limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
|
||||
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
|
||||
|
||||
@commits = @repo.commits(@ref, @path, @limit, @offset)
|
||||
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
|
||||
|
|
|
@ -178,7 +178,7 @@ class ProjectsController < ApplicationController
|
|||
def markdown_preview
|
||||
text = params[:text]
|
||||
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
|
||||
ext.analyze(text)
|
||||
|
||||
render json: {
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
class RegistrationsController < Devise::RegistrationsController
|
||||
before_action :signup_enabled?
|
||||
include Recaptcha::Verify
|
||||
|
||||
def new
|
||||
redirect_to(new_user_session_path)
|
||||
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
|
||||
DeleteUserService.new(current_user).execute(current_user)
|
||||
|
||||
|
@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
def sign_up_params
|
||||
params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
|
||||
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
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
class SessionsController < Devise::SessionsController
|
||||
include AuthenticatesWithTwoFactor
|
||||
include Recaptcha::ClientHelper
|
||||
|
||||
prepend_before_action :authenticate_with_two_factor, only: [:create]
|
||||
prepend_before_action :store_redirect_path, only: [:new]
|
||||
before_action :auto_sign_in_with_provider, only: [:new]
|
||||
before_action :load_recaptcha
|
||||
|
||||
def new
|
||||
if Gitlab.config.ldap.enabled
|
||||
|
@ -40,7 +42,7 @@ class SessionsController < Devise::SessionsController
|
|||
User.find(session[:otp_user_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def store_redirect_path
|
||||
redirect_path =
|
||||
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
|
||||
|
@ -87,14 +89,14 @@ class SessionsController < Devise::SessionsController
|
|||
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
|
||||
return unless provider.present?
|
||||
|
||||
# Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
|
||||
# registered or no alert at all. In case of another alert (such as a blocked user), it is safer
|
||||
# Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
|
||||
# registered or no alert at all. In case of another alert (such as a blocked user), it is safer
|
||||
# to do nothing to prevent redirection loops with certain Omniauth providers.
|
||||
return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated')
|
||||
|
||||
|
||||
# Prevent alert from popping up on the first page shown after authentication.
|
||||
flash[:alert] = nil
|
||||
|
||||
flash[:alert] = nil
|
||||
|
||||
redirect_to user_omniauth_authorize_path(provider.to_sym)
|
||||
end
|
||||
|
||||
|
@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController
|
|||
AuditEventService.new(user, user, options).
|
||||
for_authentication.security_event
|
||||
end
|
||||
|
||||
def load_recaptcha
|
||||
Gitlab::Recaptcha.load_configurations!
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,7 +72,7 @@ module ApplicationHelper
|
|||
if user_or_email.is_a?(User)
|
||||
user = user_or_email
|
||||
else
|
||||
user = User.find_by(email: user_or_email)
|
||||
user = User.find_by(email: user_or_email.downcase)
|
||||
end
|
||||
|
||||
if user
|
||||
|
|
|
@ -50,5 +50,17 @@ module AuthHelper
|
|||
current_user.identities.exists?(provider: provider.to_s)
|
||||
end
|
||||
|
||||
def two_factor_skippable?
|
||||
current_application_settings.require_two_factor_authentication &&
|
||||
!current_user.two_factor_enabled &&
|
||||
current_application_settings.two_factor_grace_period &&
|
||||
!two_factor_grace_period_expired?
|
||||
end
|
||||
|
||||
def two_factor_grace_period_expired?
|
||||
current_user.otp_grace_period_started_at &&
|
||||
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
||||
|
|
|
@ -69,6 +69,10 @@ module IssuesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def issue_button_visibility(issue, closed)
|
||||
return 'hidden' if issue.closed? == closed
|
||||
end
|
||||
|
||||
def issue_to_atom(xml, issue)
|
||||
xml.entry do
|
||||
xml.id namespace_project_issue_url(issue.project.namespace,
|
||||
|
@ -120,6 +124,18 @@ module IssuesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def awards_sort(awards)
|
||||
awards.sort_by do |award, notes|
|
||||
if award == "thumbsup"
|
||||
0
|
||||
elsif award == "thumbsdown"
|
||||
1
|
||||
else
|
||||
2
|
||||
end
|
||||
end.to_h
|
||||
end
|
||||
|
||||
# Required for Banzai::Filter::IssueReferenceFilter
|
||||
module_function :url_for_issue
|
||||
end
|
||||
|
|
|
@ -8,6 +8,80 @@ module PageLayoutHelper
|
|||
@page_title.join(" \u00b7 ")
|
||||
end
|
||||
|
||||
# Define or get a description for the current page
|
||||
#
|
||||
# description - String (default: nil)
|
||||
#
|
||||
# If this helper is called multiple times with an argument, only the last
|
||||
# description will be returned when called without an argument. Descriptions
|
||||
# have newlines replaced with spaces and all HTML tags are sanitized.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# page_description # => "GitLab Community Edition"
|
||||
# page_description("Foo")
|
||||
# page_description # => "Foo"
|
||||
#
|
||||
# page_description("<b>Bar</b>\nBaz")
|
||||
# page_description # => "Bar Baz"
|
||||
#
|
||||
# Returns an HTML-safe String.
|
||||
def page_description(description = nil)
|
||||
@page_description ||= page_description_default
|
||||
|
||||
if description.present?
|
||||
@page_description = description.squish
|
||||
else
|
||||
sanitize(@page_description, tags: []).truncate_words(30)
|
||||
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
|
||||
default = image_url('gitlab_logo.png')
|
||||
|
||||
if @project
|
||||
@project.avatar_url || default
|
||||
elsif @user
|
||||
avatar_icon(@user)
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
# Define or get attributes to be used as Twitter card metadata
|
||||
#
|
||||
# map - Hash of label => data pairs. Keys become labels, values become data
|
||||
#
|
||||
# Raises ArgumentError if given more than two attributes
|
||||
def page_card_attributes(map = {})
|
||||
raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
|
||||
|
||||
@page_card_attributes ||= {}
|
||||
@page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
|
||||
@page_card_attributes
|
||||
end
|
||||
|
||||
def page_card_meta_tags
|
||||
tags = ''
|
||||
|
||||
page_card_attributes.each_with_index do |pair, i|
|
||||
tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
|
||||
tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
|
||||
end
|
||||
|
||||
tags.html_safe
|
||||
end
|
||||
|
||||
def header_title(title = nil, title_url = nil)
|
||||
if title
|
||||
@header_title = title
|
||||
|
|
|
@ -132,14 +132,14 @@ class Ability
|
|||
end
|
||||
|
||||
def public_project_rules
|
||||
project_guest_rules + [
|
||||
@public_project_rules ||= project_guest_rules + [
|
||||
:download_code,
|
||||
:fork_project
|
||||
]
|
||||
end
|
||||
|
||||
def project_guest_rules
|
||||
[
|
||||
@project_guest_rules ||= [
|
||||
:read_project,
|
||||
:read_wiki,
|
||||
:read_issue,
|
||||
|
@ -157,7 +157,7 @@ class Ability
|
|||
end
|
||||
|
||||
def project_report_rules
|
||||
project_guest_rules + [
|
||||
@project_report_rules ||= project_guest_rules + [
|
||||
:create_commit_status,
|
||||
:read_commit_statuses,
|
||||
:download_code,
|
||||
|
@ -170,7 +170,7 @@ class Ability
|
|||
end
|
||||
|
||||
def project_dev_rules
|
||||
project_report_rules + [
|
||||
@project_dev_rules ||= project_report_rules + [
|
||||
:admin_merge_request,
|
||||
:create_merge_request,
|
||||
:create_wiki,
|
||||
|
@ -181,7 +181,7 @@ class Ability
|
|||
end
|
||||
|
||||
def project_archived_rules
|
||||
[
|
||||
@project_archived_rules ||= [
|
||||
:create_merge_request,
|
||||
:push_code,
|
||||
:push_code_to_protected_branches,
|
||||
|
@ -191,7 +191,7 @@ class Ability
|
|||
end
|
||||
|
||||
def project_master_rules
|
||||
project_dev_rules + [
|
||||
@project_master_rules ||= project_dev_rules + [
|
||||
:push_code_to_protected_branches,
|
||||
:update_project_snippet,
|
||||
:update_merge_request,
|
||||
|
@ -206,7 +206,7 @@ class Ability
|
|||
end
|
||||
|
||||
def project_admin_rules
|
||||
project_master_rules + [
|
||||
@project_admin_rules ||= project_master_rules + [
|
||||
:change_namespace,
|
||||
:change_visibility_level,
|
||||
:rename_project,
|
||||
|
@ -332,7 +332,7 @@ class Ability
|
|||
end
|
||||
|
||||
if snippet.public? || snippet.internal?
|
||||
rules << :read_personal_snippet
|
||||
rules << :read_personal_snippet
|
||||
end
|
||||
|
||||
rules
|
||||
|
|
|
@ -2,32 +2,34 @@
|
|||
#
|
||||
# Table name: application_settings
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# default_projects_limit :integer
|
||||
# signup_enabled :boolean
|
||||
# signin_enabled :boolean
|
||||
# gravatar_enabled :boolean
|
||||
# sign_in_text :text
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# home_page_url :string(255)
|
||||
# default_branch_protection :integer default(2)
|
||||
# twitter_sharing_enabled :boolean default(TRUE)
|
||||
# restricted_visibility_levels :text
|
||||
# version_check_enabled :boolean default(TRUE)
|
||||
# max_attachment_size :integer default(10), not null
|
||||
# default_project_visibility :integer
|
||||
# default_snippet_visibility :integer
|
||||
# restricted_signup_domains :text
|
||||
# user_oauth_applications :boolean default(TRUE)
|
||||
# after_sign_out_path :string(255)
|
||||
# session_expire_delay :integer default(10080), not null
|
||||
# import_sources :text
|
||||
# help_page_text :text
|
||||
# admin_notification_email :string(255)
|
||||
# shared_runners_enabled :boolean default(TRUE), not null
|
||||
# max_artifacts_size :integer default(100), not null
|
||||
# runners_registration_token :string(255)
|
||||
# id :integer not null, primary key
|
||||
# default_projects_limit :integer
|
||||
# signup_enabled :boolean
|
||||
# signin_enabled :boolean
|
||||
# gravatar_enabled :boolean
|
||||
# sign_in_text :text
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# home_page_url :string(255)
|
||||
# default_branch_protection :integer default(2)
|
||||
# twitter_sharing_enabled :boolean default(TRUE)
|
||||
# restricted_visibility_levels :text
|
||||
# version_check_enabled :boolean default(TRUE)
|
||||
# max_attachment_size :integer default(10), not null
|
||||
# default_project_visibility :integer
|
||||
# default_snippet_visibility :integer
|
||||
# restricted_signup_domains :text
|
||||
# user_oauth_applications :boolean default(TRUE)
|
||||
# after_sign_out_path :string(255)
|
||||
# session_expire_delay :integer default(10080), not null
|
||||
# import_sources :text
|
||||
# help_page_text :text
|
||||
# admin_notification_email :string(255)
|
||||
# shared_runners_enabled :boolean default(TRUE), not null
|
||||
# max_artifacts_size :integer default(100), not null
|
||||
# runners_registration_token :string(255)
|
||||
# require_two_factor_authentication :boolean default(TRUE)
|
||||
# two_factor_grace_period :integer default(48)
|
||||
#
|
||||
|
||||
class ApplicationSetting < ActiveRecord::Base
|
||||
|
@ -42,21 +44,32 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
attr_accessor :restricted_signup_domains_raw
|
||||
|
||||
validates :session_expire_delay,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validates :home_page_url,
|
||||
allow_blank: true,
|
||||
url: true,
|
||||
if: :home_page_url_column_exist
|
||||
allow_blank: true,
|
||||
url: true,
|
||||
if: :home_page_url_column_exist
|
||||
|
||||
validates :after_sign_out_path,
|
||||
allow_blank: true,
|
||||
url: true
|
||||
allow_blank: true,
|
||||
url: true
|
||||
|
||||
validates :admin_notification_email,
|
||||
allow_blank: true,
|
||||
email: true
|
||||
allow_blank: true,
|
||||
email: true
|
||||
|
||||
validates :two_factor_grace_period,
|
||||
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|
|
||||
unless value.nil?
|
||||
|
@ -112,6 +125,8 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
|
||||
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
|
||||
max_artifacts_size: Settings.artifacts['max_size'],
|
||||
require_two_factor_authentication: false,
|
||||
two_factor_grace_period: 48
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -194,8 +194,11 @@ module Ci
|
|||
end
|
||||
|
||||
def raw_trace
|
||||
if File.exist?(path_to_trace)
|
||||
if File.file?(path_to_trace)
|
||||
File.read(path_to_trace)
|
||||
elsif project.ci_id && File.file?(old_path_to_trace)
|
||||
# Temporary fix for build trace data integrity
|
||||
File.read(old_path_to_trace)
|
||||
else
|
||||
# backward compatibility
|
||||
read_attribute :trace
|
||||
|
@ -212,8 +215,8 @@ module Ci
|
|||
end
|
||||
|
||||
def trace=(trace)
|
||||
unless Dir.exists? dir_to_trace
|
||||
FileUtils.mkdir_p dir_to_trace
|
||||
unless Dir.exists?(dir_to_trace)
|
||||
FileUtils.mkdir_p(dir_to_trace)
|
||||
end
|
||||
|
||||
File.write(path_to_trace, trace)
|
||||
|
@ -231,6 +234,55 @@ module Ci
|
|||
"#{dir_to_trace}/#{id}.log"
|
||||
end
|
||||
|
||||
##
|
||||
# Deprecated
|
||||
#
|
||||
# This is a hotfix for CI build data integrity, see #4246
|
||||
# Should be removed in 8.4, after CI files migration has been done.
|
||||
#
|
||||
def old_dir_to_trace
|
||||
File.join(
|
||||
Settings.gitlab_ci.builds_path,
|
||||
created_at.utc.strftime("%Y_%m"),
|
||||
project.ci_id.to_s
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Deprecated
|
||||
#
|
||||
# This is a hotfix for CI build data integrity, see #4246
|
||||
# Should be removed in 8.4, after CI files migration has been done.
|
||||
#
|
||||
def old_path_to_trace
|
||||
"#{old_dir_to_trace}/#{id}.log"
|
||||
end
|
||||
|
||||
##
|
||||
# Deprecated
|
||||
#
|
||||
# This contains a hotfix for CI build data integrity, see #4246
|
||||
#
|
||||
# This method is used by `ArtifactUploader` to create a store_dir.
|
||||
# Warning: Uploader uses it after AND before file has been stored.
|
||||
#
|
||||
# This method returns old path to artifacts only if it already exists.
|
||||
#
|
||||
def artifacts_path
|
||||
old = File.join(created_at.utc.strftime('%Y_%m'),
|
||||
project.ci_id.to_s,
|
||||
id.to_s)
|
||||
|
||||
old_store = File.join(ArtifactUploader.artifacts_path, old)
|
||||
return old if project.ci_id && File.directory?(old_store)
|
||||
|
||||
File.join(
|
||||
created_at.utc.strftime('%Y_%m'),
|
||||
project.id.to_s,
|
||||
id.to_s
|
||||
)
|
||||
end
|
||||
|
||||
def token
|
||||
project.runners_token
|
||||
end
|
||||
|
|
|
@ -95,14 +95,12 @@ module Issuable
|
|||
opened? || reopened?
|
||||
end
|
||||
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def downvotes
|
||||
0
|
||||
notes.awards.where(note: "thumbsdown").count
|
||||
end
|
||||
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def upvotes
|
||||
0
|
||||
notes.awards.where(note: "thumbsup").count
|
||||
end
|
||||
|
||||
def subscribed?(user)
|
||||
|
@ -161,6 +159,14 @@ module Issuable
|
|||
self.class.to_s.underscore
|
||||
end
|
||||
|
||||
# Returns a Hash of attributes to be used for Twitter card metadata
|
||||
def card_attributes
|
||||
{
|
||||
'Author' => author.try(:name),
|
||||
'Assignee' => assignee.try(:name)
|
||||
}
|
||||
end
|
||||
|
||||
def notes_with_associations
|
||||
notes.includes(:author, :project)
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ module Mentionable
|
|||
end
|
||||
|
||||
def all_references(current_user = self.author, text = nil)
|
||||
ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
|
||||
ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author)
|
||||
|
||||
if text
|
||||
ext.analyze(text)
|
||||
|
|
|
@ -107,9 +107,16 @@ class Note < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def grouped_awards
|
||||
notes = {}
|
||||
|
||||
awards.select(:note).distinct.map do |note|
|
||||
[ note.note, where(note: note.note) ]
|
||||
notes[note.note] = where(note: note.note)
|
||||
end
|
||||
|
||||
notes["thumbsup"] ||= Note.none
|
||||
notes["thumbsdown"] ||= Note.none
|
||||
|
||||
notes
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -339,14 +346,12 @@ class Note < ActiveRecord::Base
|
|||
read_attribute(:system)
|
||||
end
|
||||
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def downvote?
|
||||
false
|
||||
is_award && note == "thumbsdown"
|
||||
end
|
||||
|
||||
# Deprecated. Still exists to preserve API compatibility.
|
||||
def upvote?
|
||||
false
|
||||
is_award && note == "thumbsup"
|
||||
end
|
||||
|
||||
def editable?
|
||||
|
|
|
@ -555,7 +555,9 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def send_move_instructions(old_path_with_namespace)
|
||||
NotificationService.new.project_was_moved(self, old_path_with_namespace)
|
||||
# New project path needs to be committed to the DB or notification will
|
||||
# retrieve stale information
|
||||
run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
|
||||
end
|
||||
|
||||
def owner
|
||||
|
|
|
@ -76,7 +76,9 @@ class Repository
|
|||
path: path,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
follow: path.present?
|
||||
# --follow doesn't play well with --skip. See:
|
||||
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
|
||||
follow: false
|
||||
}
|
||||
|
||||
commits = Gitlab::Git::Commit.where(options)
|
||||
|
|
|
@ -20,16 +20,12 @@ class ArtifactUploader < CarrierWave::Uploader::Base
|
|||
@build, @field = build, field
|
||||
end
|
||||
|
||||
def artifacts_path
|
||||
File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s)
|
||||
end
|
||||
|
||||
def store_dir
|
||||
File.join(ArtifactUploader.artifacts_path, artifacts_path)
|
||||
File.join(self.class.artifacts_path, @build.artifacts_path)
|
||||
end
|
||||
|
||||
def cache_dir
|
||||
File.join(ArtifactUploader.artifacts_cache_path, artifacts_path)
|
||||
File.join(self.class.artifacts_cache_path, @build.artifacts_path)
|
||||
end
|
||||
|
||||
def file_storage?
|
||||
|
|
|
@ -104,6 +104,18 @@
|
|||
= f.label :signin_enabled do
|
||||
= f.check_box :signin_enabled
|
||||
Sign-in enabled
|
||||
.form-group
|
||||
= f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
.checkbox
|
||||
= f.label :require_two_factor_authentication do
|
||||
= f.check_box :require_two_factor_authentication
|
||||
Require all users to setup Two-Factor authentication
|
||||
.form-group
|
||||
= f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
|
||||
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
|
||||
.form-group
|
||||
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
|
@ -144,5 +156,82 @@
|
|||
.col-sm-10
|
||||
= f.number_field :max_artifacts_size, class: 'form-control'
|
||||
|
||||
%fieldset
|
||||
%legend Metrics
|
||||
%p
|
||||
These settings require a restart to take effect.
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
.checkbox
|
||||
= f.label :metrics_enabled do
|
||||
= f.check_box :metrics_enabled
|
||||
Enable InfluxDB Metrics
|
||||
.form-group
|
||||
= f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
|
||||
.form-group
|
||||
= f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
|
||||
.help-block
|
||||
The UDP port to use for connecting to InfluxDB. InfluxDB requires that
|
||||
your server configuration specifies a database to store data in when
|
||||
sending messages to this port, without it metrics data will not be
|
||||
saved.
|
||||
.form-group
|
||||
= f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :metrics_username, class: 'form-control'
|
||||
.form-group
|
||||
= f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :metrics_password, class: 'form-control'
|
||||
.form-group
|
||||
= f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :metrics_pool_size, class: 'form-control'
|
||||
.help-block
|
||||
The amount of InfluxDB connections to open. Connections are opened
|
||||
lazily. Users using multi-threaded application servers should ensure
|
||||
enough connections are available (at minimum the amount of application
|
||||
server threads).
|
||||
.form-group
|
||||
= f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :metrics_timeout, class: 'form-control'
|
||||
.help-block
|
||||
The amount of seconds after which an InfluxDB connection will time
|
||||
out.
|
||||
.form-group
|
||||
= f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :metrics_method_call_threshold, class: 'form-control'
|
||||
.help-block
|
||||
A method call is only tracked when it takes longer to complete than
|
||||
the given amount of milliseconds.
|
||||
|
||||
%fieldset
|
||||
%legend Spam and Anti-bot Protection
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
.checkbox
|
||||
= f.label :recaptcha_enabled do
|
||||
= f.check_box :recaptcha_enabled
|
||||
Enable reCAPTCHA
|
||||
%span.help-block#recaptcha_help_block Helps preventing bots from creating accounts
|
||||
|
||||
.form-group
|
||||
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :recaptcha_site_key, class: 'form-control'
|
||||
.help-block
|
||||
Generate site and private keys here:
|
||||
%a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
|
||||
.form-group
|
||||
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :recaptcha_private_key, class: 'form-control'
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Save', class: 'btn btn-primary'
|
||||
|
|
|
@ -6,17 +6,21 @@
|
|||
.login-heading
|
||||
%h3 Create an account
|
||||
.login-body
|
||||
- user = params[:user].present? ? params[:user] : {}
|
||||
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
%div
|
||||
= f.text_field :name, class: "form-control top", placeholder: "Name", required: true
|
||||
= f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true
|
||||
%div
|
||||
= f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
|
||||
= f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true
|
||||
%div
|
||||
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
|
||||
= f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true
|
||||
.form-group.append-bottom-20#password-strength
|
||||
= f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
|
||||
= f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
|
||||
%div
|
||||
- if current_application_settings.recaptcha_enabled
|
||||
= recaptcha_tags
|
||||
%div
|
||||
= f.submit "Sign up", class: "btn-create btn"
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
.col-sm-10
|
||||
.checkbox
|
||||
= f.check_box :public
|
||||
%span.descr Make this group public (even if there is no any public project inside this group)
|
||||
%span.descr Make this group public (even if there are no public projects inside this group)
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Save group', class: "btn btn-save"
|
||||
|
|
|
@ -219,11 +219,3 @@
|
|||
%td.shortcut
|
||||
.key r
|
||||
%td Reply (quoting selected text)
|
||||
|
||||
|
||||
:javascript
|
||||
$('.js-more-help-button').click(function (e) {
|
||||
$(this).remove()l
|
||||
$('.hidden-shortcut').show();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
- page_title "GitLab"
|
||||
%head
|
||||
%head{prefix: "og: http://ogp.me/ns#"}
|
||||
%meta{charset: "utf-8"}
|
||||
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
|
||||
%meta{content: "GitLab Community Edition", name: "description"}
|
||||
%meta{name: 'referrer', content: 'origin-when-cross-origin'}
|
||||
|
||||
%meta{name: "description", content: page_description}
|
||||
|
||||
-# Open Graph - http://ogp.me/
|
||||
%meta{property: 'og:type', content: "object"}
|
||||
%meta{property: 'og:site_name', content: "GitLab"}
|
||||
%meta{property: 'og:title', content: page_title}
|
||||
%meta{property: 'og:description', content: page_description}
|
||||
%meta{property: 'og:image', content: page_image}
|
||||
%meta{property: 'og:url', content: request.base_url + request.fullpath}
|
||||
|
||||
-# Twitter Card - https://dev.twitter.com/cards/types/summary
|
||||
%meta{property: 'twitter:card', content: "summary"}
|
||||
%meta{property: 'twitter:title', content: page_title}
|
||||
%meta{property: 'twitter:description', content: page_description}
|
||||
%meta{property: 'twitter:image', content: page_image}
|
||||
= page_card_meta_tags
|
||||
|
||||
- page_title "GitLab"
|
||||
%title= page_title
|
||||
|
||||
= favicon_link_tag 'favicon.ico'
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
comment = val.match(/^\S+ \S+ (.+)\n?$/);
|
||||
|
||||
if( comment && comment.length > 1 && title.val() == '' ){
|
||||
$('#key_title').val( comment[1] );
|
||||
$('#key_title').val( comment[1] ).change();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -38,3 +38,4 @@
|
|||
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
|
||||
.form-actions
|
||||
= submit_tag 'Submit', class: 'btn btn-success'
|
||||
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
|
||||
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
|
||||
- page_description @issue.description
|
||||
- page_card_attributes @issue.card_attributes
|
||||
|
||||
= render "header_title"
|
||||
|
||||
.issue
|
||||
.detail-page-header
|
||||
.status-box{ class: status_box_class(@issue) }
|
||||
- if @issue.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
.status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
|
||||
.status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
|
||||
%span.identifier
|
||||
Issue ##{@issue.iid}
|
||||
%span.creator
|
||||
|
@ -27,10 +27,8 @@
|
|||
= icon('plus')
|
||||
New Issue
|
||||
- if can?(current_user, :update_issue, @issue)
|
||||
- if @issue.closed?
|
||||
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen'
|
||||
- else
|
||||
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close Issue'
|
||||
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
|
||||
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
|
||||
|
||||
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
|
||||
= icon('pencil-square-o')
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
|
||||
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
|
||||
- page_description @merge_request.description
|
||||
- page_card_attributes @merge_request.card_attributes
|
||||
|
||||
= render "header_title"
|
||||
|
||||
- if params[:view] == 'parallel'
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
- page_title @milestone.title, "Milestones"
|
||||
- page_title @milestone.title, "Milestones"
|
||||
- page_description @milestone.description
|
||||
|
||||
= render "header_title"
|
||||
|
||||
.detail-page-header
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- page_title @user.name
|
||||
- header_title @user.name, user_path(@user)
|
||||
- page_title @user.name
|
||||
- page_description @user.bio
|
||||
- header_title @user.name, user_path(@user)
|
||||
|
||||
= content_for :meta_tags do
|
||||
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.awards.votes-block
|
||||
- votable.notes.awards.grouped_awards.each do |emoji, notes|
|
||||
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
|
||||
.award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
|
||||
= emoji_icon(emoji)
|
||||
.counter
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<%= ENV['RAILS_ENV'] %>:
|
||||
adapter: <%= ENV['GITLAB_DATABASE_ADAPTER'] || 'postgresql' %>
|
||||
encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %>
|
||||
database: <%= ENV['GITLAB_DATABASE_DATABASE'] || "gitlab_#{ENV['RAILS_ENV']}" %>
|
||||
pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %>
|
||||
username: <%= ENV['GITLAB_DATABASE_USERNAME'] || 'root' %>
|
||||
password: <%= ENV['GITLAB_DATABASE_PASSWORD'] || '' %>
|
||||
host: <%= ENV['GITLAB_DATABASE_HOST'] || 'localhost' %>
|
||||
port: <%= ENV['GITLAB_DATABASE_PORT'] || '5432' %>
|
|
@ -4,8 +4,8 @@
|
|||
#
|
||||
########################### NOTE #####################################
|
||||
# This file should not receive new settings. All configuration options #
|
||||
# that do not require an application restart are being moved to #
|
||||
# ApplicationSetting model! #
|
||||
# * are being moved to ApplicationSetting model! #
|
||||
# If a setting requires an application restart say so in that screen. #
|
||||
# If you change this file in a Merge Request, please also create #
|
||||
# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests #
|
||||
########################################################################
|
||||
|
|
|
@ -131,6 +131,7 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours
|
|||
Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
|
||||
Settings.omniauth.session_tickets['cas3'] = 'ticket'
|
||||
|
||||
|
||||
Settings['shared'] ||= Settingslogic.new({})
|
||||
Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
|
||||
|
||||
|
@ -144,7 +145,7 @@ Settings.gitlab['default_projects_limit'] ||= 10
|
|||
Settings.gitlab['default_branch_protection'] ||= 2
|
||||
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
|
||||
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
|
||||
Settings.gitlab['host'] ||= 'localhost'
|
||||
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
|
||||
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
|
||||
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
|
||||
Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
if Gitlab::Metrics.enabled?
|
||||
require 'influxdb'
|
||||
require 'socket'
|
||||
require 'connection_pool'
|
||||
require 'method_source'
|
||||
|
||||
# These are manually require'd so the classes are registered properly with
|
||||
# ActiveSupport.
|
||||
require 'gitlab/metrics/subscribers/action_view'
|
||||
require 'gitlab/metrics/subscribers/active_record'
|
||||
|
||||
Gitlab::Application.configure do |config|
|
||||
config.middleware.use(Gitlab::Metrics::RackMiddleware)
|
||||
end
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.server_middleware do |chain|
|
||||
chain.add Gitlab::Metrics::SidekiqMiddleware
|
||||
end
|
||||
end
|
||||
|
||||
# This instruments all methods residing in app/models that (appear to) use any
|
||||
# of the ActiveRecord methods. This has to take place _after_ initializing as
|
||||
# for some unknown reason calling eager_load! earlier breaks Devise.
|
||||
Gitlab::Application.config.after_initialize do
|
||||
Rails.application.eager_load!
|
||||
|
||||
models = Rails.root.join('app', 'models').to_s
|
||||
|
||||
regex = Regexp.union(
|
||||
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
|
||||
)
|
||||
|
||||
Gitlab::Metrics::Instrumentation.
|
||||
instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
|
||||
# Instrumenting the ApplicationSetting class can lead to an infinite
|
||||
# loop. Since the data is cached any way we don't really need to
|
||||
# instrument it.
|
||||
if klass == ApplicationSetting
|
||||
false
|
||||
else
|
||||
loc = method.source_location
|
||||
|
||||
loc && loc[0].start_with?(models) && method.source =~ regex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Metrics::Instrumentation.configure do |config|
|
||||
config.instrument_instance_methods(Gitlab::Shell)
|
||||
|
||||
config.instrument_methods(Gitlab::Git)
|
||||
|
||||
Gitlab::Git.constants.each do |name|
|
||||
const = Gitlab::Git.const_get(name)
|
||||
|
||||
config.instrument_methods(const) if const.is_a?(Module)
|
||||
end
|
||||
end
|
||||
|
||||
GC::Profiler.enable
|
||||
|
||||
Gitlab::Metrics::Sampler.new.start
|
||||
end
|
|
@ -297,6 +297,7 @@ Rails.application.routes.draw do
|
|||
resource :two_factor_auth, only: [:new, :create, :destroy] do
|
||||
member do
|
||||
post :codes
|
||||
patch :skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
class AddTfaToApplicationSettings < ActiveRecord::Migration
|
||||
def change
|
||||
change_table :application_settings do |t|
|
||||
t.boolean :require_two_factor_authentication, default: false
|
||||
t.integer :two_factor_grace_period, default: 48
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
class AddTfaAdditionalFields < ActiveRecord::Migration
|
||||
def change
|
||||
change_table :users do |t|
|
||||
t.datetime :otp_grace_period_started_at, null: true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
class InfluxdbSettings < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :application_settings, :metrics_enabled, :boolean, default: false
|
||||
|
||||
add_column :application_settings, :metrics_host, :string,
|
||||
default: 'localhost'
|
||||
|
||||
add_column :application_settings, :metrics_database, :string,
|
||||
default: 'gitlab'
|
||||
|
||||
add_column :application_settings, :metrics_username, :string
|
||||
add_column :application_settings, :metrics_password, :string
|
||||
add_column :application_settings, :metrics_pool_size, :integer, default: 16
|
||||
add_column :application_settings, :metrics_timeout, :integer, default: 10
|
||||
add_column :application_settings, :metrics_method_call_threshold,
|
||||
:integer, default: 10
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
class AddRecaptchaToApplicationSettings < ActiveRecord::Migration
|
||||
def change
|
||||
change_table :application_settings do |t|
|
||||
t.boolean :recaptcha_enabled, default: false
|
||||
t.string :recaptcha_site_key
|
||||
t.string :recaptcha_private_key
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class InfluxdbUdpPortSetting < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :application_settings, :metrics_port, :integer, default: 8089
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration
|
||||
def change
|
||||
remove_column :application_settings, :metrics_database
|
||||
end
|
||||
end
|
80
db/schema.rb
80
db/schema.rb
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20151224123230) do
|
||||
ActiveRecord::Schema.define(version: 20151229112614) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -33,23 +33,36 @@ ActiveRecord::Schema.define(version: 20151224123230) do
|
|||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "home_page_url"
|
||||
t.integer "default_branch_protection", default: 2
|
||||
t.boolean "twitter_sharing_enabled", default: true
|
||||
t.integer "default_branch_protection", default: 2
|
||||
t.boolean "twitter_sharing_enabled", default: true
|
||||
t.text "restricted_visibility_levels"
|
||||
t.boolean "version_check_enabled", default: true
|
||||
t.integer "max_attachment_size", default: 10, null: false
|
||||
t.boolean "version_check_enabled", default: true
|
||||
t.integer "max_attachment_size", default: 10, null: false
|
||||
t.integer "default_project_visibility"
|
||||
t.integer "default_snippet_visibility"
|
||||
t.text "restricted_signup_domains"
|
||||
t.boolean "user_oauth_applications", default: true
|
||||
t.boolean "user_oauth_applications", default: true
|
||||
t.string "after_sign_out_path"
|
||||
t.integer "session_expire_delay", default: 10080, null: false
|
||||
t.integer "session_expire_delay", default: 10080, null: false
|
||||
t.text "import_sources"
|
||||
t.text "help_page_text"
|
||||
t.string "admin_notification_email"
|
||||
t.boolean "shared_runners_enabled", default: true, null: false
|
||||
t.integer "max_artifacts_size", default: 100, null: false
|
||||
t.boolean "shared_runners_enabled", default: true, null: false
|
||||
t.integer "max_artifacts_size", default: 100, null: false
|
||||
t.string "runners_registration_token"
|
||||
t.boolean "require_two_factor_authentication", default: false
|
||||
t.integer "two_factor_grace_period", default: 48
|
||||
t.boolean "metrics_enabled", default: false
|
||||
t.string "metrics_host", default: "localhost"
|
||||
t.string "metrics_username"
|
||||
t.string "metrics_password"
|
||||
t.integer "metrics_pool_size", default: 16
|
||||
t.integer "metrics_timeout", default: 10
|
||||
t.integer "metrics_method_call_threshold", default: 10
|
||||
t.boolean "recaptcha_enabled", default: false
|
||||
t.string "recaptcha_site_key"
|
||||
t.string "recaptcha_private_key"
|
||||
t.integer "metrics_port", default: 8089
|
||||
end
|
||||
|
||||
create_table "audit_events", force: :cascade do |t|
|
||||
|
@ -783,12 +796,12 @@ ActiveRecord::Schema.define(version: 20151224123230) do
|
|||
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "reset_password_token"
|
||||
t.datetime "reset_password_sent_at"
|
||||
t.datetime "remember_created_at"
|
||||
t.integer "sign_in_count", default: 0
|
||||
t.integer "sign_in_count", default: 0
|
||||
t.datetime "current_sign_in_at"
|
||||
t.datetime "last_sign_in_at"
|
||||
t.string "current_sign_in_ip"
|
||||
|
@ -796,22 +809,22 @@ ActiveRecord::Schema.define(version: 20151224123230) do
|
|||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "name"
|
||||
t.boolean "admin", default: false, null: false
|
||||
t.integer "projects_limit", default: 10
|
||||
t.string "skype", default: "", null: false
|
||||
t.string "linkedin", default: "", null: false
|
||||
t.string "twitter", default: "", null: false
|
||||
t.boolean "admin", default: false, null: false
|
||||
t.integer "projects_limit", default: 10
|
||||
t.string "skype", default: "", null: false
|
||||
t.string "linkedin", default: "", null: false
|
||||
t.string "twitter", default: "", null: false
|
||||
t.string "authentication_token"
|
||||
t.integer "theme_id", default: 1, null: false
|
||||
t.integer "theme_id", default: 1, null: false
|
||||
t.string "bio"
|
||||
t.integer "failed_attempts", default: 0
|
||||
t.integer "failed_attempts", default: 0
|
||||
t.datetime "locked_at"
|
||||
t.string "username"
|
||||
t.boolean "can_create_group", default: true, null: false
|
||||
t.boolean "can_create_team", default: true, null: false
|
||||
t.boolean "can_create_group", default: true, null: false
|
||||
t.boolean "can_create_team", default: true, null: false
|
||||
t.string "state"
|
||||
t.integer "color_scheme_id", default: 1, null: false
|
||||
t.integer "notification_level", default: 1, null: false
|
||||
t.integer "color_scheme_id", default: 1, null: false
|
||||
t.integer "notification_level", default: 1, null: false
|
||||
t.datetime "password_expires_at"
|
||||
t.integer "created_by_id"
|
||||
t.datetime "last_credential_check_at"
|
||||
|
@ -820,24 +833,25 @@ ActiveRecord::Schema.define(version: 20151224123230) do
|
|||
t.datetime "confirmed_at"
|
||||
t.datetime "confirmation_sent_at"
|
||||
t.string "unconfirmed_email"
|
||||
t.boolean "hide_no_ssh_key", default: false
|
||||
t.string "website_url", default: "", null: false
|
||||
t.boolean "hide_no_ssh_key", default: false
|
||||
t.string "website_url", default: "", null: false
|
||||
t.string "notification_email"
|
||||
t.boolean "hide_no_password", default: false
|
||||
t.boolean "password_automatically_set", default: false
|
||||
t.boolean "hide_no_password", default: false
|
||||
t.boolean "password_automatically_set", default: false
|
||||
t.string "location"
|
||||
t.string "encrypted_otp_secret"
|
||||
t.string "encrypted_otp_secret_iv"
|
||||
t.string "encrypted_otp_secret_salt"
|
||||
t.boolean "otp_required_for_login", default: false, null: false
|
||||
t.boolean "otp_required_for_login", default: false, null: false
|
||||
t.text "otp_backup_codes"
|
||||
t.string "public_email", default: "", null: false
|
||||
t.integer "dashboard", default: 0
|
||||
t.integer "project_view", default: 0
|
||||
t.string "public_email", default: "", null: false
|
||||
t.integer "dashboard", default: 0
|
||||
t.integer "project_view", default: 0
|
||||
t.integer "consumed_timestep"
|
||||
t.integer "layout", default: 0
|
||||
t.boolean "hide_project_limit", default: false
|
||||
t.integer "layout", default: 0
|
||||
t.boolean "hide_project_limit", default: false
|
||||
t.string "unlock_token"
|
||||
t.datetime "otp_grace_period_started_at"
|
||||
end
|
||||
|
||||
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
|
||||
|
|
|
@ -28,17 +28,18 @@
|
|||
- [Using SSH keys](ci/ssh_keys/README.md)
|
||||
- [User permissions](ci/permissions/README.md)
|
||||
- [API](ci/api/README.md)
|
||||
- [Triggering builds through the API](ci/triggers/README.md)
|
||||
|
||||
### CI Languages
|
||||
|
||||
+ [Testing PHP](ci/languages/php.md)
|
||||
- [Testing PHP](ci/languages/php.md)
|
||||
|
||||
### CI Services
|
||||
|
||||
+ [Using MySQL](ci/services/mysql.md)
|
||||
+ [Using PostgreSQL](ci/services/postgres.md)
|
||||
+ [Using Redis](ci/services/redis.md)
|
||||
+ [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
|
||||
- [Using MySQL](ci/services/mysql.md)
|
||||
- [Using PostgreSQL](ci/services/postgres.md)
|
||||
- [Using Redis](ci/services/redis.md)
|
||||
- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
|
||||
|
||||
### CI Examples
|
||||
|
||||
|
@ -55,6 +56,7 @@
|
|||
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
|
||||
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
|
||||
- [Log system](logs/logs.md) Log system.
|
||||
- [Environmental Variables](administration/environmental_variables.md) to configure GitLab.
|
||||
- [Operations](operations/README.md) Keeping GitLab up and running
|
||||
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
|
||||
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# Environment Variables
|
||||
|
||||
## Introduction
|
||||
|
||||
Commonly people configure GitLab via the gitlab.rb configuration file in the Omnibus package.
|
||||
|
||||
But if you prefer to use environment variables we allow that too.
|
||||
|
||||
## Supported environment variables
|
||||
|
||||
Variable | Type | Explanation
|
||||
--- | --- | ---
|
||||
GITLAB_ROOT_PASSWORD | string | sets the password for the `root` user on installation
|
||||
GITLAB_HOST | url | hostname of the GitLab server includes http or https
|
||||
RAILS_ENV | production/development/staging/test | Rails environment
|
||||
DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5
|
||||
|
||||
## Complete database variables
|
||||
|
||||
As explained in the [Heroku documentation](https://devcenter.heroku.com/articles/rails-database-connection-behavior) the DATABASE_URL doesn't let you set:
|
||||
|
||||
- adapter
|
||||
- database
|
||||
- username
|
||||
- password
|
||||
- host
|
||||
- port
|
||||
|
||||
To do so please `cp config/database.yml.env config/database.yml` and use the following variables:
|
||||
|
||||
Variable | Default
|
||||
--- | ---
|
||||
GITLAB_DATABASE_ADAPTER | postgresql
|
||||
GITLAB_DATABASE_ENCODING | unicode
|
||||
GITLAB_DATABASE_DATABASE | gitlab_#{ENV['RAILS_ENV']
|
||||
GITLAB_DATABASE_POOL | 10
|
||||
GITLAB_DATABASE_USERNAME | root
|
||||
GITLAB_DATABASE_PASSWORD |
|
||||
GITLAB_DATABASE_HOST | localhost
|
||||
GITLAB_DATABASE_PORT | 5432
|
||||
|
||||
## Other variables
|
||||
|
||||
We welcome merge requests to make more settings configurable via variables.
|
||||
Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}".
|
|
@ -4,8 +4,7 @@
|
|||
|
||||
Get all merge requests for this project.
|
||||
The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
|
||||
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and
|
||||
`downvotes` are deprecated and always return `0`.
|
||||
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
|
||||
|
||||
```
|
||||
GET /projects/:id/merge_requests
|
||||
|
@ -58,7 +57,7 @@ Parameters:
|
|||
|
||||
## Get single MR
|
||||
|
||||
Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`.
|
||||
Shows information about a single merge request.
|
||||
|
||||
```
|
||||
GET /projects/:id/merge_request/:merge_request_id
|
||||
|
@ -141,8 +140,6 @@ Parameters:
|
|||
## Get single MR changes
|
||||
|
||||
Shows information about the merge request including its files and changes.
|
||||
With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and
|
||||
always return `0`.
|
||||
|
||||
```
|
||||
GET /projects/:id/merge_request/:merge_request_id/changes
|
||||
|
@ -213,9 +210,7 @@ Parameters:
|
|||
|
||||
## Create MR
|
||||
|
||||
Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and `
|
||||
downvotes` are deprecated and always return `0`.
|
||||
|
||||
Creates a new merge request.
|
||||
```
|
||||
POST /projects/:id/merge_requests
|
||||
```
|
||||
|
@ -266,8 +261,7 @@ If an error occurs, an error number and a message explaining the reason is retur
|
|||
|
||||
## Update MR
|
||||
|
||||
Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes`
|
||||
are deprecated and always return `0`.
|
||||
Updates an existing merge request. You can change the target branch, title, or even close the MR.
|
||||
|
||||
```
|
||||
PUT /projects/:id/merge_request/:merge_request_id
|
||||
|
@ -318,8 +312,7 @@ If an error occurs, an error number and a message explaining the reason is retur
|
|||
|
||||
## Accept MR
|
||||
|
||||
Merge changes submitted with MR using this API. With GitLab 8.2 the return
|
||||
fields `upvotes` and `downvotes` are deprecated and always return `0`.
|
||||
Merge changes submitted with MR using this API.
|
||||
|
||||
If merge success you get `200 OK`.
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@ Notes are comments on snippets, issues or merge requests.
|
|||
|
||||
### List project issue notes
|
||||
|
||||
Gets a list of all notes for a single issue. With GitLab 8.2 the return fields
|
||||
`upvote` and `downvote` are deprecated and always return `false`.
|
||||
Gets a list of all notes for a single issue.
|
||||
|
||||
```
|
||||
GET /projects/:id/issues/:issue_id/notes
|
||||
|
|
|
@ -90,7 +90,17 @@ GET /users
|
|||
|
||||
You can search for users by email or username with: `/users?search=John`
|
||||
|
||||
Also see `def search query` in `app/models/user.rb`.
|
||||
In addition, you can lookup users by username:
|
||||
|
||||
```
|
||||
GET /users?username=:username
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
GET /users?username=jack_smith
|
||||
```
|
||||
|
||||
## Single user
|
||||
|
||||
|
|
|
@ -2,25 +2,26 @@
|
|||
|
||||
### User documentation
|
||||
|
||||
+ [Quick Start](quick_start/README.md)
|
||||
+ [Configuring project (.gitlab-ci.yml)](yaml/README.md)
|
||||
+ [Configuring runner](runners/README.md)
|
||||
+ [Configuring deployment](deployment/README.md)
|
||||
+ [Using Docker Images](docker/using_docker_images.md)
|
||||
+ [Using Docker Build](docker/using_docker_build.md)
|
||||
+ [Using Variables](variables/README.md)
|
||||
+ [Using SSH keys](ssh_keys/README.md)
|
||||
* [Quick Start](quick_start/README.md)
|
||||
* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
|
||||
* [Configuring runner](runners/README.md)
|
||||
* [Configuring deployment](deployment/README.md)
|
||||
* [Using Docker Images](docker/using_docker_images.md)
|
||||
* [Using Docker Build](docker/using_docker_build.md)
|
||||
* [Using Variables](variables/README.md)
|
||||
* [Using SSH keys](ssh_keys/README.md)
|
||||
* [Triggering builds through the API](triggers/README.md)
|
||||
|
||||
### Languages
|
||||
|
||||
+ [Testing PHP](languages/php.md)
|
||||
* [Testing PHP](languages/php.md)
|
||||
|
||||
### Services
|
||||
|
||||
+ [Using MySQL](services/mysql.md)
|
||||
+ [Using PostgreSQL](services/postgres.md)
|
||||
+ [Using Redis](services/redis.md)
|
||||
+ [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
|
||||
* [Using MySQL](services/mysql.md)
|
||||
* [Using PostgreSQL](services/postgres.md)
|
||||
* [Using Redis](services/redis.md)
|
||||
* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -32,5 +33,5 @@
|
|||
|
||||
### Administrator documentation
|
||||
|
||||
+ [User permissions](permissions/README.md)
|
||||
+ [API](api/README.md)
|
||||
* [User permissions](permissions/README.md)
|
||||
* [API](api/README.md)
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
# Triggering Builds through the API
|
||||
|
||||
_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_
|
||||
|
||||
Triggers can be used to force a rebuild of a specific branch, tag or commit,
|
||||
with an API call.
|
||||
|
||||
## Add a trigger
|
||||
|
||||
You can add a new trigger by going to your project's **Settings > Triggers**.
|
||||
The **Add trigger** button will create a new token which you can then use to
|
||||
trigger a rebuild of this particular project.
|
||||
|
||||
Every new trigger you create, gets assigned a different token which you can
|
||||
then use inside your scripts or `.gitlab-ci.yml`. You also have a nice
|
||||
overview of the time the triggers were last used.
|
||||
|
||||
![Triggers page overview](img/triggers_page.png)
|
||||
|
||||
## Revoke a trigger
|
||||
|
||||
You can revoke a trigger any time by going at your project's
|
||||
**Settings > Triggers** and hitting the **Revoke** button. The action is
|
||||
irreversible.
|
||||
|
||||
## Trigger a build
|
||||
|
||||
To trigger a build you need to send a `POST` request to GitLab's API endpoint:
|
||||
|
||||
```
|
||||
POST /projects/:id/trigger/builds
|
||||
```
|
||||
|
||||
The required parameters are the trigger's `token` and the Git `ref` on which
|
||||
the trigger will be performed. Valid refs are the branch, the tag or the commit
|
||||
SHA. The `:id` of a project can be found by [querying the API](../api/projects.md)
|
||||
or by visiting the **Triggers** page which provides self-explanatory examples.
|
||||
|
||||
When a rebuild is triggered, the information is exposed in GitLab's UI under
|
||||
the **Builds** page and the builds are marked as `triggered`.
|
||||
|
||||
![Marked rebuilds as triggered on builds page](img/builds_page.png)
|
||||
|
||||
---
|
||||
|
||||
You can see which trigger caused the rebuild by visiting the single build page.
|
||||
The token of the trigger is exposed in the UI as you can see from the image
|
||||
below.
|
||||
|
||||
![Marked rebuilds as triggered on a single build page](img/trigger_single_build.png)
|
||||
|
||||
---
|
||||
|
||||
See the [Examples](#examples) section for more details on how to actually
|
||||
trigger a rebuild.
|
||||
|
||||
## Pass build variables to a trigger
|
||||
|
||||
You can pass any number of arbitrary variables in the trigger API call and they
|
||||
will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
|
||||
file. The parameter is of the form:
|
||||
|
||||
```
|
||||
variables[key]=value
|
||||
```
|
||||
|
||||
This information is also exposed in the UI.
|
||||
|
||||
![Build variables in UI](img/trigger_variables.png)
|
||||
|
||||
---
|
||||
|
||||
See the [Examples](#examples) section below for more details.
|
||||
|
||||
## Examples
|
||||
|
||||
Using cURL you can trigger a rebuild with minimal effort, for example:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-F token=TOKEN \
|
||||
-F ref=master \
|
||||
https://gitlab.example.com/api/v3/projects/9/trigger/builds
|
||||
```
|
||||
|
||||
In this case, the project with ID `9` will get rebuilt on `master` branch.
|
||||
|
||||
|
||||
### Triggering a build within `.gitlab-ci.yml`
|
||||
|
||||
You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
|
||||
you have two projects, A and B, and you want to trigger a rebuild on the `master`
|
||||
branch of project B whenever a tag on project A is created. This is the job you
|
||||
need to add in project's A `.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
build_docs:
|
||||
stage: deploy
|
||||
script:
|
||||
- "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
|
||||
only:
|
||||
- tags
|
||||
```
|
||||
|
||||
Now, whenever a new tag is pushed on project A, the build will run and the
|
||||
`build_docs` job will be executed, triggering a rebuild of project B. The
|
||||
`stage: deploy` ensures that this job will run only after all jobs with
|
||||
`stage: test` complete successfully.
|
||||
|
||||
_**Note:** If your project is public, passing the token in plain text is
|
||||
probably not the wisest idea, so you might want to use a
|
||||
[secure variable](../variables/README.md#user-defined-variables-secure-variables)
|
||||
for that purpose._
|
||||
|
||||
### Making use of trigger variables
|
||||
|
||||
Using trigger variables can be proven useful for a variety of reasons.
|
||||
|
||||
* Identifiable jobs. Since the variable is exposed in the UI you can know
|
||||
why the rebuild was triggered if you pass a variable that explains the
|
||||
purpose.
|
||||
* Conditional job processing. You can have conditional jobs that run whenever
|
||||
a certain variable is present.
|
||||
|
||||
Consider the following `.gitlab-ci.yml` where we set three
|
||||
[stages](../yaml/README.md#stages) and the `upload_package` job is run only
|
||||
when all jobs from the test and build stages pass. When the `UPLOAD_TO_S3`
|
||||
variable is non-zero, `make upload` is run.
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- package
|
||||
|
||||
run_tests:
|
||||
script:
|
||||
- make test
|
||||
|
||||
build_package:
|
||||
stage: build
|
||||
script:
|
||||
- make build
|
||||
|
||||
upload_package:
|
||||
stage: package
|
||||
script:
|
||||
- if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi
|
||||
```
|
||||
|
||||
You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable
|
||||
and the script of the `upload_package` job will run:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-F token=TOKEN \
|
||||
-F ref=master \
|
||||
-F "variables[UPLOAD_TO_S3]=true" \
|
||||
https://gitlab.example.com/api/v3/projects/9/trigger/builds
|
||||
```
|
||||
|
||||
### Using cron to trigger nightly builds
|
||||
|
||||
Whether you craft a script or just run cURL directly, you can trigger builds
|
||||
in conjunction with cron. The example below triggers a build on the `master`
|
||||
branch of project with ID `9` every night at `00:30`:
|
||||
|
||||
```bash
|
||||
30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
|
||||
```
|
||||
|
||||
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
|
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -13,6 +13,7 @@ See the documentation below for details on how to configure these services.
|
|||
- [Slack](slack.md) Integrate with the Slack chat service
|
||||
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
|
||||
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
|
||||
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
|
||||
|
||||
GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html).
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# reCAPTCHA
|
||||
|
||||
GitLab leverages [Google's reCAPTCHA](https://www.google.com/recaptcha/intro/index.html)
|
||||
to protect against spam and abuse. GitLab displays the CAPTCHA form on the sign-up page
|
||||
to confirm that a real user, not a bot, is attempting to create an account.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use reCAPTCHA, first you must create a site and private key.
|
||||
|
||||
1. Go to the URL: https://www.google.com/recaptcha/admin
|
||||
|
||||
2. Fill out the form necessary to obtain reCAPTCHA keys.
|
||||
|
||||
3. Login to your GitLab server, with administrator credentials.
|
||||
|
||||
4. Go to Applications Settings on Admin Area (`admin/application_settings`)
|
||||
|
||||
5. Fill all recaptcha fields with keys from previous steps
|
||||
|
||||
6. Check the `Enable reCAPTCHA` checkbox
|
||||
|
||||
7. Save the configuration.
|
|
@ -6,11 +6,11 @@ If a user is both in a project group and in the project itself, the highest perm
|
|||
|
||||
If a user is a GitLab administrator they receive all permissions.
|
||||
|
||||
On public projects the Guest role is not enforced.
|
||||
All users will be able to create issues, leave comments, and pull or download the project code.
|
||||
On public projects the Guest role is not enforced.
|
||||
All users will be able to create issues, leave comments, and pull or download the project code.
|
||||
|
||||
To add or import a user, you can follow the [project users and members
|
||||
documentation](doc/workflow/add-user/add-user.md).
|
||||
documentation](../workflow/add-user/add-user.md).
|
||||
|
||||
## Project
|
||||
|
||||
|
|
|
@ -6,3 +6,5 @@
|
|||
- [Information exclusivity](information_exclusivity.md)
|
||||
- [Reset your root password](reset_root_password.md)
|
||||
- [User File Uploads](user_file_uploads.md)
|
||||
- [How we manage the CRIME vulnerability](crime_vulnerability.md)
|
||||
- [Enforce Two-Factor authentication](two_factor_authentication.md)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# How we manage the TLS protocol CRIME vulnerability
|
||||
|
||||
> CRIME ("Compression Ratio Info-leak Made Easy") is a security exploit against
|
||||
secret web cookies over connections using the HTTPS and SPDY protocols that also
|
||||
use data compression. When used to recover the content of secret
|
||||
authentication cookies, it allows an attacker to perform session hijacking on an
|
||||
authenticated web session, allowing the launching of further attacks.
|
||||
([CRIME](https://en.wikipedia.org/w/index.php?title=CRIME&oldid=692423806))
|
||||
|
||||
### Description
|
||||
|
||||
The TLS Protocol CRIME Vulnerability affects compression over HTTPS, therefore
|
||||
it warns against using SSL Compression (for example gzip) or SPDY which
|
||||
optionally uses compression as well.
|
||||
|
||||
GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME
|
||||
vulnerability by deactivating gzip when HTTPS is enabled. You can see the
|
||||
sources of the files in question:
|
||||
|
||||
* [Source installation NGINX file][source-nginx]
|
||||
* [Omnibus installation NGINX file][omnibus-nginx]
|
||||
|
||||
Although SPDY is enabled in Omnibus installations, CRIME relies on compression
|
||||
(the 'C') and the default compression level in NGINX's SPDY module is 0
|
||||
(no compression).
|
||||
|
||||
### Nessus
|
||||
|
||||
The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab
|
||||
similar to the following format:
|
||||
|
||||
```
|
||||
Description
|
||||
|
||||
This remote service has one of two configurations that are known to be required for the CRIME attack:
|
||||
SSL/TLS compression is enabled.
|
||||
TLS advertises the SPDY protocol earlier than version 4.
|
||||
|
||||
...
|
||||
|
||||
Output
|
||||
|
||||
The following configuration indicates that the remote service may be vulnerable to the CRIME attack:
|
||||
SPDY support earlier than version 4 is advertised.
|
||||
```
|
||||
|
||||
From the report above it is important to note that Nessus is only checking if
|
||||
TLS advertises the SPDY protocol earlier than version 4, it does not perform an
|
||||
attack nor does it check if compression is enabled. With just this approach, it
|
||||
cannot tell that SPDY's compression is disabled and not subject to the CRIME
|
||||
vulnerability.
|
||||
|
||||
### References
|
||||
|
||||
* Nginx ["Module ngx_http_spdy_module"][ngx-spdy]
|
||||
* Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus]
|
||||
* Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia
|
||||
|
||||
[source-nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/gitlab-ssl
|
||||
[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb
|
||||
[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html
|
||||
[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565
|
||||
[wiki-crime]: https://en.wikipedia.org/wiki/CRIME
|
|
@ -0,0 +1,38 @@
|
|||
# Enforce Two-factor Authentication (2FA)
|
||||
|
||||
Two-factor Authentication (2FA) provides an additional level of security to your
|
||||
users' GitLab account. Once enabled, in addition to supplying their username and
|
||||
password to login, they'll be prompted for a code generated by an application on
|
||||
their phone.
|
||||
|
||||
You can read more about it here:
|
||||
[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
|
||||
|
||||
## Enabling 2FA
|
||||
|
||||
Users on GitLab, can enable it without any admin's intervention. If you want to
|
||||
enforce everyone to setup 2FA, you can choose from two different ways:
|
||||
|
||||
1. Enforce on next login
|
||||
2. Suggest on next login, but allow a grace period before enforcing.
|
||||
|
||||
In the Admin area under **Settings** (`/admin/application_settings`), look for
|
||||
the "Sign-in Restrictions" area, where you can configure both.
|
||||
|
||||
If you want 2FA enforcement to take effect on next login, change the grace
|
||||
period to `0`
|
||||
|
||||
## Disabling 2FA for everyone
|
||||
|
||||
There may be some special situations where you want to disable 2FA for everyone
|
||||
even when forced 2FA is disabled. There is a rake task for that:
|
||||
|
||||
```
|
||||
# use this command if you've installed GitLab with the Omnibus package
|
||||
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
|
||||
|
||||
# if you've installed GitLab from source
|
||||
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
|
||||
```
|
||||
|
||||
**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.**
|
|
@ -99,8 +99,6 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
|
|||
# Clean up assets and cache
|
||||
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
|
||||
|
||||
# Update init.d script
|
||||
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
|
||||
```
|
||||
|
||||
### 7. Update configuration files
|
||||
|
|
|
@ -15,15 +15,17 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I click to emoji in the picker' do
|
||||
page.within '.emoji-menu' do
|
||||
page.within '.emoji-menu-content' do
|
||||
page.first('.emoji-icon').click
|
||||
end
|
||||
end
|
||||
|
||||
step 'I can remove it by clicking to icon' do
|
||||
page.within '.awards' do
|
||||
page.first('.award').click
|
||||
expect(page).to_not have_selector '.award'
|
||||
expect do
|
||||
page.find('.award.active').click
|
||||
sleep 0.1
|
||||
end.to change{ page.all(".award").size }.from(3).to(2)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,7 +39,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
|
|||
step 'I have award added' do
|
||||
page.within '.awards' do
|
||||
expect(page).to have_selector '.award'
|
||||
expect(page.find('.award .counter')).to have_content '1'
|
||||
expect(page.find('.award.active .counter')).to have_content '1'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -166,7 +166,6 @@ module API
|
|||
|
||||
class MergeRequest < ProjectEntity
|
||||
expose :target_branch, :source_branch
|
||||
# deprecated, always returns 0
|
||||
expose :upvotes, :downvotes
|
||||
expose :author, :assignee, using: Entities::UserBasic
|
||||
expose :source_project_id, :target_project_id
|
||||
|
|
|
@ -8,11 +8,17 @@ module API
|
|||
#
|
||||
# Example Request:
|
||||
# GET /users
|
||||
# GET /users?search=Admin
|
||||
# GET /users?username=root
|
||||
get do
|
||||
@users = User.all
|
||||
@users = @users.active if params[:active].present?
|
||||
@users = @users.search(params[:search]) if params[:search].present?
|
||||
@users = paginate @users
|
||||
if params[:username].present?
|
||||
@users = User.where(username: params[:username])
|
||||
else
|
||||
@users = User.all
|
||||
@users = @users.active if params[:active].present?
|
||||
@users = @users.search(params[:search]) if params[:search].present?
|
||||
@users = paginate @users
|
||||
end
|
||||
|
||||
if current_user.is_admin?
|
||||
present @users, with: Entities::UserFull
|
||||
|
|
|
@ -98,7 +98,7 @@ module Banzai
|
|||
project = project_from_ref(project_ref)
|
||||
|
||||
if project && object = find_object(project, id)
|
||||
title = escape_once(object_link_title(object))
|
||||
title = object_link_title(object)
|
||||
klass = reference_class(object_sym)
|
||||
|
||||
data = data_attribute(
|
||||
|
@ -110,17 +110,11 @@ module Banzai
|
|||
url = matches[:url] if matches.names.include?("url")
|
||||
url ||= url_for_object(object, project)
|
||||
|
||||
text = link_text
|
||||
unless text
|
||||
text = object.reference_link_text(context[:project])
|
||||
|
||||
extras = object_link_text_extras(object, matches)
|
||||
text += " (#{extras.join(", ")})" if extras.any?
|
||||
end
|
||||
text = link_text || object_link_text(object, matches)
|
||||
|
||||
%(<a href="#{url}" #{data}
|
||||
title="#{title}"
|
||||
class="#{klass}">#{text}</a>)
|
||||
title="#{escape_once(title)}"
|
||||
class="#{klass}">#{escape_once(text)}</a>)
|
||||
else
|
||||
match
|
||||
end
|
||||
|
@ -140,6 +134,15 @@ module Banzai
|
|||
def object_link_title(object)
|
||||
"#{object_class.name.titleize}: #{object.title}"
|
||||
end
|
||||
|
||||
def object_link_text(object, matches)
|
||||
text = object.reference_link_text(context[:project])
|
||||
|
||||
extras = object_link_text_extras(object, matches)
|
||||
text += " (#{extras.join(", ")})" if extras.any?
|
||||
|
||||
text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -63,15 +63,15 @@ module Banzai
|
|||
|
||||
url = url_for_issue(id, project, only_path: context[:only_path])
|
||||
|
||||
title = escape_once("Issue in #{project.external_issue_tracker.title}")
|
||||
title = "Issue in #{project.external_issue_tracker.title}"
|
||||
klass = reference_class(:issue)
|
||||
data = data_attribute(project: project.id, external_issue: id)
|
||||
|
||||
text = link_text || match
|
||||
|
||||
%(<a href="#{url}" #{data}
|
||||
title="#{title}"
|
||||
class="#{klass}">#{text}</a>)
|
||||
title="#{escape_once(title)}"
|
||||
class="#{klass}">#{escape_once(text)}</a>)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ module Banzai
|
|||
text = link_text || render_colored_label(label)
|
||||
|
||||
%(<a href="#{url}" #{data}
|
||||
class="#{klass}">#{text}</a>)
|
||||
class="#{klass}">#{escape_once(text)}</a>)
|
||||
else
|
||||
match
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module Banzai
|
|||
class RedactorFilter < HTML::Pipeline::Filter
|
||||
def call
|
||||
doc.css('a.gfm').each do |node|
|
||||
unless user_can_reference?(node)
|
||||
unless user_can_see_reference?(node)
|
||||
# The reference should be replaced by the original text,
|
||||
# which is not always the same as the rendered text.
|
||||
text = node.attr('data-original') || node.text
|
||||
|
@ -24,12 +24,12 @@ module Banzai
|
|||
|
||||
private
|
||||
|
||||
def user_can_reference?(node)
|
||||
def user_can_see_reference?(node)
|
||||
if node.has_attribute?('data-reference-filter')
|
||||
reference_type = node.attr('data-reference-filter')
|
||||
reference_filter = Banzai::Filter.const_get(reference_type)
|
||||
|
||||
reference_filter.user_can_reference?(current_user, node, context)
|
||||
reference_filter.user_can_see_reference?(current_user, node, context)
|
||||
else
|
||||
true
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ module Banzai
|
|||
# :project (required) - Current project, ignored if reference is cross-project.
|
||||
# :only_path - Generate path-only links.
|
||||
class ReferenceFilter < HTML::Pipeline::Filter
|
||||
def self.user_can_reference?(user, node, context)
|
||||
def self.user_can_see_reference?(user, node, context)
|
||||
if node.has_attribute?('data-project')
|
||||
project_id = node.attr('data-project').to_i
|
||||
return true if project_id == context[:project].try(:id)
|
||||
|
@ -24,6 +24,10 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.user_can_reference?(user, node, context)
|
||||
true
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
raise NotImplementedError, "#{self} does not implement #{__method__}"
|
||||
end
|
||||
|
@ -44,11 +48,11 @@ module Banzai
|
|||
# Returns a String
|
||||
def data_attribute(attributes = {})
|
||||
attributes[:reference_filter] = self.class.name.demodulize
|
||||
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
|
||||
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
|
||||
end
|
||||
|
||||
def escape_once(html)
|
||||
ERB::Util.html_escape_once(html)
|
||||
html.html_safe? ? html : ERB::Util.html_escape_once(html)
|
||||
end
|
||||
|
||||
def ignore_parents
|
||||
|
|
|
@ -35,7 +35,9 @@ module Banzai
|
|||
|
||||
return if context[:reference_filter] && reference_filter != context[:reference_filter]
|
||||
|
||||
return unless reference_filter.user_can_reference?(current_user, node, context)
|
||||
return if author && !reference_filter.user_can_reference?(author, node, context)
|
||||
|
||||
return unless reference_filter.user_can_see_reference?(current_user, node, context)
|
||||
|
||||
references = reference_filter.referenced_by(node)
|
||||
return unless references
|
||||
|
@ -57,6 +59,10 @@ module Banzai
|
|||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
||||
def author
|
||||
context[:author]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.user_can_reference?(user, node, context)
|
||||
def self.user_can_see_reference?(user, node, context)
|
||||
if node.has_attribute?('data-group')
|
||||
group = Group.find(node.attr('data-group')) rescue nil
|
||||
Ability.abilities.allowed?(user, :read_group, group)
|
||||
|
@ -48,6 +48,18 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.user_can_reference?(user, node, context)
|
||||
# Only team members can reference `@all`
|
||||
if node.has_attribute?('data-project')
|
||||
project = Project.find(node.attr('data-project')) rescue nil
|
||||
return false unless project
|
||||
|
||||
user && project.team.member?(user)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def call
|
||||
replace_text_nodes_matching(User.reference_pattern) do |content|
|
||||
user_link_filter(content)
|
||||
|
@ -122,7 +134,7 @@ module Banzai
|
|||
end
|
||||
|
||||
def link_tag(url, data, text)
|
||||
%(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
|
||||
%(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
extend Gitlab::CurrentSettings
|
||||
|
||||
RAILS_ROOT = Rails.root.to_s
|
||||
METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
|
||||
PATH_REGEX = /^#{RAILS_ROOT}\/?/
|
||||
|
||||
def self.pool_size
|
||||
current_application_settings[:metrics_pool_size] || 16
|
||||
end
|
||||
|
||||
def self.timeout
|
||||
current_application_settings[:metrics_timeout] || 10
|
||||
end
|
||||
|
||||
def self.enabled?
|
||||
current_application_settings[:metrics_enabled] || false
|
||||
end
|
||||
|
||||
def self.mri?
|
||||
RUBY_ENGINE == 'ruby'
|
||||
end
|
||||
|
||||
def self.method_call_threshold
|
||||
# This is memoized since this method is called for every instrumented
|
||||
# method. Loading data from an external cache on every method call slows
|
||||
# things down too much.
|
||||
@method_call_threshold ||=
|
||||
(current_application_settings[:metrics_method_call_threshold] || 10)
|
||||
end
|
||||
|
||||
def self.pool
|
||||
@pool
|
||||
end
|
||||
|
||||
def self.hostname
|
||||
@hostname
|
||||
end
|
||||
|
||||
# Returns a relative path and line number based on the last application call
|
||||
# frame.
|
||||
def self.last_relative_application_frame
|
||||
frame = caller_locations.find do |l|
|
||||
l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT)
|
||||
end
|
||||
|
||||
if frame
|
||||
return frame.path.sub(PATH_REGEX, ''), frame.lineno
|
||||
else
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
def self.submit_metrics(metrics)
|
||||
prepared = prepare_metrics(metrics)
|
||||
|
||||
pool.with do |connection|
|
||||
prepared.each do |metric|
|
||||
begin
|
||||
connection.write_points([metric])
|
||||
rescue StandardError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.prepare_metrics(metrics)
|
||||
metrics.map do |hash|
|
||||
new_hash = hash.symbolize_keys
|
||||
|
||||
new_hash[:tags].each do |key, value|
|
||||
if value.blank?
|
||||
new_hash[:tags].delete(key)
|
||||
else
|
||||
new_hash[:tags][key] = escape_value(value)
|
||||
end
|
||||
end
|
||||
|
||||
new_hash
|
||||
end
|
||||
end
|
||||
|
||||
def self.escape_value(value)
|
||||
value.to_s.gsub('=', '\\=')
|
||||
end
|
||||
|
||||
@hostname = Socket.gethostname
|
||||
|
||||
# When enabled this should be set before being used as the usual pattern
|
||||
# "@foo ||= bar" is _not_ thread-safe.
|
||||
if enabled?
|
||||
@pool = ConnectionPool.new(size: pool_size, timeout: timeout) do
|
||||
host = current_application_settings[:metrics_host]
|
||||
user = current_application_settings[:metrics_username]
|
||||
pw = current_application_settings[:metrics_password]
|
||||
port = current_application_settings[:metrics_port]
|
||||
|
||||
InfluxDB::Client.
|
||||
new(udp: { host: host, port: port }, username: user, password: pw)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Class for calculating the difference between two numeric values.
|
||||
#
|
||||
# Every call to `compared_with` updates the internal value. This makes it
|
||||
# possible to use a single Delta instance to calculate the delta over time
|
||||
# of an ever increasing number.
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# delta = Delta.new(0)
|
||||
#
|
||||
# delta.compared_with(10) # => 10
|
||||
# delta.compared_with(15) # => 5
|
||||
# delta.compared_with(20) # => 5
|
||||
class Delta
|
||||
def initialize(value = 0)
|
||||
@value = value
|
||||
end
|
||||
|
||||
# new_value - The value to compare with as a Numeric.
|
||||
#
|
||||
# Returns a new Numeric (depending on the type of `new_value`).
|
||||
def compared_with(new_value)
|
||||
delta = new_value - @value
|
||||
@value = new_value
|
||||
|
||||
delta
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,146 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Module for instrumenting methods.
|
||||
#
|
||||
# This module allows instrumenting of methods without having to actually
|
||||
# alter the target code (e.g. by including modules).
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
|
||||
module Instrumentation
|
||||
SERIES = 'method_calls'
|
||||
|
||||
def self.configure
|
||||
yield self
|
||||
end
|
||||
|
||||
# Instruments a class method.
|
||||
#
|
||||
# mod - The module to instrument as a Module/Class.
|
||||
# name - The name of the method to instrument.
|
||||
def self.instrument_method(mod, name)
|
||||
instrument(:class, mod, name)
|
||||
end
|
||||
|
||||
# Instruments an instance method.
|
||||
#
|
||||
# mod - The module to instrument as a Module/Class.
|
||||
# name - The name of the method to instrument.
|
||||
def self.instrument_instance_method(mod, name)
|
||||
instrument(:instance, mod, name)
|
||||
end
|
||||
|
||||
# Recursively instruments all subclasses of the given root module.
|
||||
#
|
||||
# This can be used to for example instrument all ActiveRecord models (as
|
||||
# these all inherit from ActiveRecord::Base).
|
||||
#
|
||||
# This method can optionally take a block to pass to `instrument_methods`
|
||||
# and `instrument_instance_methods`.
|
||||
#
|
||||
# root - The root module for which to instrument subclasses. The root
|
||||
# module itself is not instrumented.
|
||||
def self.instrument_class_hierarchy(root, &block)
|
||||
visit = root.subclasses
|
||||
|
||||
until visit.empty?
|
||||
klass = visit.pop
|
||||
|
||||
instrument_methods(klass, &block)
|
||||
instrument_instance_methods(klass, &block)
|
||||
|
||||
klass.subclasses.each { |c| visit << c }
|
||||
end
|
||||
end
|
||||
|
||||
# Instruments all public methods of a module.
|
||||
#
|
||||
# This method optionally takes a block that can be used to determine if a
|
||||
# method should be instrumented or not. The block is passed the receiving
|
||||
# module and an UnboundMethod. If the block returns a non truthy value the
|
||||
# method is not instrumented.
|
||||
#
|
||||
# mod - The module to instrument.
|
||||
def self.instrument_methods(mod)
|
||||
mod.public_methods(false).each do |name|
|
||||
method = mod.method(name)
|
||||
|
||||
if method.owner == mod.singleton_class
|
||||
if !block_given? || block_given? && yield(mod, method)
|
||||
instrument_method(mod, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Instruments all public instance methods of a module.
|
||||
#
|
||||
# See `instrument_methods` for more information.
|
||||
#
|
||||
# mod - The module to instrument.
|
||||
def self.instrument_instance_methods(mod)
|
||||
mod.public_instance_methods(false).each do |name|
|
||||
method = mod.instance_method(name)
|
||||
|
||||
if method.owner == mod
|
||||
if !block_given? || block_given? && yield(mod, method)
|
||||
instrument_instance_method(mod, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Instruments a method.
|
||||
#
|
||||
# type - The type (:class or :instance) of method to instrument.
|
||||
# mod - The module containing the method.
|
||||
# name - The name of the method to instrument.
|
||||
def self.instrument(type, mod, name)
|
||||
return unless Metrics.enabled?
|
||||
|
||||
name = name.to_sym
|
||||
alias_name = :"_original_#{name}"
|
||||
target = type == :instance ? mod : mod.singleton_class
|
||||
|
||||
if type == :instance
|
||||
target = mod
|
||||
label = "#{mod.name}##{name}"
|
||||
else
|
||||
target = mod.singleton_class
|
||||
label = "#{mod.name}.#{name}"
|
||||
end
|
||||
|
||||
target.class_eval <<-EOF, __FILE__, __LINE__ + 1
|
||||
alias_method #{alias_name.inspect}, #{name.inspect}
|
||||
|
||||
def #{name}(*args, &block)
|
||||
trans = Gitlab::Metrics::Instrumentation.transaction
|
||||
|
||||
if trans
|
||||
start = Time.now
|
||||
retval = __send__(#{alias_name.inspect}, *args, &block)
|
||||
duration = (Time.now - start) * 1000.0
|
||||
|
||||
if duration >= Gitlab::Metrics.method_call_threshold
|
||||
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
|
||||
{ duration: duration },
|
||||
method: #{label.inspect})
|
||||
end
|
||||
|
||||
retval
|
||||
else
|
||||
__send__(#{alias_name.inspect}, *args, &block)
|
||||
end
|
||||
end
|
||||
EOF
|
||||
end
|
||||
|
||||
# Small layer of indirection to make it easier to stub out the current
|
||||
# transaction.
|
||||
def self.transaction
|
||||
Transaction.current
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Class for storing details of a single metric (label, value, etc).
|
||||
class Metric
|
||||
attr_reader :series, :values, :tags, :created_at
|
||||
|
||||
# series - The name of the series (as a String) to store the metric in.
|
||||
# values - A Hash containing the values to store.
|
||||
# tags - A Hash containing extra tags to add to the metrics.
|
||||
def initialize(series, values, tags = {})
|
||||
@values = values
|
||||
@series = series
|
||||
@tags = tags
|
||||
@created_at = Time.now.utc
|
||||
end
|
||||
|
||||
# Returns a Hash in a format that can be directly written to InfluxDB.
|
||||
def to_hash
|
||||
{
|
||||
series: @series,
|
||||
tags: @tags.merge(
|
||||
hostname: Metrics.hostname,
|
||||
ruby_engine: RUBY_ENGINE,
|
||||
ruby_version: RUBY_VERSION,
|
||||
gitlab_version: Gitlab::VERSION,
|
||||
process_type: Sidekiq.server? ? 'sidekiq' : 'rails'
|
||||
),
|
||||
values: @values,
|
||||
timestamp: @created_at.to_i * 1_000_000_000
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Class for producing SQL queries with sensitive data stripped out.
|
||||
class ObfuscatedSQL
|
||||
REPLACEMENT = /
|
||||
\d+(\.\d+)? # integers, floats
|
||||
| '.+?' # single quoted strings
|
||||
| \/.+?(?<!\\)\/ # regexps (including escaped slashes)
|
||||
/x
|
||||
|
||||
MYSQL_REPLACEMENTS = /
|
||||
".+?" # double quoted strings
|
||||
/x
|
||||
|
||||
# Regex to replace consecutive placeholders with a single one indicating
|
||||
# the length. This can be useful when a "IN" statement uses thousands of
|
||||
# IDs (storing this would just be a waste of space).
|
||||
CONSECUTIVE = /(\?(\s*,\s*)?){2,}/
|
||||
|
||||
# sql - The raw SQL query as a String.
|
||||
def initialize(sql)
|
||||
@sql = sql
|
||||
end
|
||||
|
||||
# Returns a new, obfuscated SQL query.
|
||||
def to_s
|
||||
regex = REPLACEMENT
|
||||
|
||||
if Gitlab::Database.mysql?
|
||||
regex = Regexp.union(regex, MYSQL_REPLACEMENTS)
|
||||
end
|
||||
|
||||
sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match|
|
||||
"#{match.count(',') + 1} values"
|
||||
end
|
||||
|
||||
# InfluxDB escapes double quotes upon output, so lets get rid of them
|
||||
# whenever we can.
|
||||
if Gitlab::Database.postgresql?
|
||||
sql = sql.delete('"')
|
||||
end
|
||||
|
||||
sql.tr("\n", ' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Rack middleware for tracking Rails requests.
|
||||
class RackMiddleware
|
||||
CONTROLLER_KEY = 'action_controller.instance'
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
# env - A Hash containing Rack environment details.
|
||||
def call(env)
|
||||
trans = transaction_from_env(env)
|
||||
retval = nil
|
||||
|
||||
begin
|
||||
retval = trans.run { @app.call(env) }
|
||||
|
||||
# Even in the event of an error we want to submit any metrics we
|
||||
# might've gathered up to this point.
|
||||
ensure
|
||||
if env[CONTROLLER_KEY]
|
||||
tag_controller(trans, env)
|
||||
end
|
||||
|
||||
trans.finish
|
||||
end
|
||||
|
||||
retval
|
||||
end
|
||||
|
||||
def transaction_from_env(env)
|
||||
trans = Transaction.new
|
||||
|
||||
trans.add_tag(:request_method, env['REQUEST_METHOD'])
|
||||
trans.add_tag(:request_uri, env['REQUEST_URI'])
|
||||
|
||||
trans
|
||||
end
|
||||
|
||||
def tag_controller(trans, env)
|
||||
controller = env[CONTROLLER_KEY]
|
||||
label = "#{controller.class.name}##{controller.action_name}"
|
||||
|
||||
trans.add_tag(:action, label)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,98 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Class that sends certain metrics to InfluxDB at a specific interval.
|
||||
#
|
||||
# This class is used to gather statistics that can't be directly associated
|
||||
# with a transaction such as system memory usage, garbage collection
|
||||
# statistics, etc.
|
||||
class Sampler
|
||||
# interval - The sampling interval in seconds.
|
||||
def initialize(interval = 15)
|
||||
@interval = interval
|
||||
@metrics = []
|
||||
|
||||
@last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
|
||||
@last_major_gc = Delta.new(GC.stat[:major_gc_count])
|
||||
|
||||
if Gitlab::Metrics.mri?
|
||||
require 'allocations'
|
||||
|
||||
Allocations.start
|
||||
end
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.new do
|
||||
Thread.current.abort_on_exception = true
|
||||
|
||||
loop do
|
||||
sleep(@interval)
|
||||
|
||||
sample
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sample
|
||||
sample_memory_usage
|
||||
sample_file_descriptors
|
||||
sample_objects
|
||||
sample_gc
|
||||
|
||||
flush
|
||||
ensure
|
||||
GC::Profiler.clear
|
||||
@metrics.clear
|
||||
end
|
||||
|
||||
def flush
|
||||
Metrics.submit_metrics(@metrics.map(&:to_hash))
|
||||
end
|
||||
|
||||
def sample_memory_usage
|
||||
@metrics << Metric.new('memory_usage', value: System.memory_usage)
|
||||
end
|
||||
|
||||
def sample_file_descriptors
|
||||
@metrics << Metric.
|
||||
new('file_descriptors', value: System.file_descriptor_count)
|
||||
end
|
||||
|
||||
if Metrics.mri?
|
||||
def sample_objects
|
||||
sample = Allocations.to_hash
|
||||
counts = sample.each_with_object({}) do |(klass, count), hash|
|
||||
hash[klass.name] = count
|
||||
end
|
||||
|
||||
# Symbols aren't allocated so we'll need to add those manually.
|
||||
counts['Symbol'] = Symbol.all_symbols.length
|
||||
|
||||
counts.each do |name, count|
|
||||
@metrics << Metric.new('object_counts', { count: count }, type: name)
|
||||
end
|
||||
end
|
||||
else
|
||||
def sample_objects
|
||||
end
|
||||
end
|
||||
|
||||
def sample_gc
|
||||
time = GC::Profiler.total_time * 1000.0
|
||||
stats = GC.stat.merge(total_time: time)
|
||||
|
||||
# We want the difference of GC runs compared to the last sample, not the
|
||||
# total amount since the process started.
|
||||
stats[:minor_gc_count] =
|
||||
@last_minor_gc.compared_with(stats[:minor_gc_count])
|
||||
|
||||
stats[:major_gc_count] =
|
||||
@last_major_gc.compared_with(stats[:major_gc_count])
|
||||
|
||||
stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
|
||||
|
||||
@metrics << Metric.new('gc_statistics', stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Sidekiq middleware for tracking jobs.
|
||||
#
|
||||
# This middleware is intended to be used as a server-side middleware.
|
||||
class SidekiqMiddleware
|
||||
def call(worker, message, queue)
|
||||
trans = Transaction.new
|
||||
|
||||
begin
|
||||
trans.run { yield }
|
||||
ensure
|
||||
tag_worker(trans, worker)
|
||||
trans.finish
|
||||
end
|
||||
end
|
||||
|
||||
def tag_worker(trans, worker)
|
||||
trans.add_tag(:action, "#{worker.class.name}#perform")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
module Subscribers
|
||||
# Class for tracking the rendering timings of views.
|
||||
class ActionView < ActiveSupport::Subscriber
|
||||
attach_to :action_view
|
||||
|
||||
SERIES = 'views'
|
||||
|
||||
def render_template(event)
|
||||
track(event) if current_transaction
|
||||
end
|
||||
|
||||
alias_method :render_view, :render_template
|
||||
|
||||
private
|
||||
|
||||
def track(event)
|
||||
values = values_for(event)
|
||||
tags = tags_for(event)
|
||||
|
||||
current_transaction.add_metric(SERIES, values, tags)
|
||||
end
|
||||
|
||||
def relative_path(path)
|
||||
path.gsub(/^#{Rails.root.to_s}\/?/, '')
|
||||
end
|
||||
|
||||
def values_for(event)
|
||||
{ duration: event.duration }
|
||||
end
|
||||
|
||||
def tags_for(event)
|
||||
path = relative_path(event.payload[:identifier])
|
||||
tags = { view: path }
|
||||
|
||||
file, line = Metrics.last_relative_application_frame
|
||||
|
||||
if file and line
|
||||
tags[:file] = file
|
||||
tags[:line] = line
|
||||
end
|
||||
|
||||
tags
|
||||
end
|
||||
|
||||
def current_transaction
|
||||
Transaction.current
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
module Subscribers
|
||||
# Class for tracking raw SQL queries.
|
||||
#
|
||||
# Queries are obfuscated before being logged to ensure no private data is
|
||||
# exposed via InfluxDB/Grafana.
|
||||
class ActiveRecord < ActiveSupport::Subscriber
|
||||
attach_to :active_record
|
||||
|
||||
SERIES = 'sql_queries'
|
||||
|
||||
def sql(event)
|
||||
return unless current_transaction
|
||||
|
||||
values = values_for(event)
|
||||
tags = tags_for(event)
|
||||
|
||||
current_transaction.add_metric(SERIES, values, tags)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def values_for(event)
|
||||
{ duration: event.duration }
|
||||
end
|
||||
|
||||
def tags_for(event)
|
||||
sql = ObfuscatedSQL.new(event.payload[:sql]).to_s
|
||||
tags = { sql: sql }
|
||||
|
||||
file, line = Metrics.last_relative_application_frame
|
||||
|
||||
if file and line
|
||||
tags[:file] = file
|
||||
tags[:line] = line
|
||||
end
|
||||
|
||||
tags
|
||||
end
|
||||
|
||||
def current_transaction
|
||||
Transaction.current
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Module for gathering system/process statistics such as the memory usage.
|
||||
#
|
||||
# This module relies on the /proc filesystem being available. If /proc is
|
||||
# not available the methods of this module will be stubbed.
|
||||
module System
|
||||
if File.exist?('/proc')
|
||||
# Returns the current process' memory usage in bytes.
|
||||
def self.memory_usage
|
||||
mem = 0
|
||||
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
|
||||
|
||||
if match and match[1]
|
||||
mem = match[1].to_f * 1024
|
||||
end
|
||||
|
||||
mem
|
||||
end
|
||||
|
||||
def self.file_descriptor_count
|
||||
Dir.glob('/proc/self/fd/*').length
|
||||
end
|
||||
else
|
||||
def self.memory_usage
|
||||
0.0
|
||||
end
|
||||
|
||||
def self.file_descriptor_count
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,66 @@
|
|||
module Gitlab
|
||||
module Metrics
|
||||
# Class for storing metrics information of a single transaction.
|
||||
class Transaction
|
||||
THREAD_KEY = :_gitlab_metrics_transaction
|
||||
|
||||
SERIES = 'transactions'
|
||||
|
||||
attr_reader :uuid, :tags
|
||||
|
||||
def self.current
|
||||
Thread.current[THREAD_KEY]
|
||||
end
|
||||
|
||||
# name - The name of this transaction as a String.
|
||||
def initialize
|
||||
@metrics = []
|
||||
@uuid = SecureRandom.uuid
|
||||
|
||||
@started_at = nil
|
||||
@finished_at = nil
|
||||
|
||||
@tags = {}
|
||||
end
|
||||
|
||||
def duration
|
||||
@finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
|
||||
end
|
||||
|
||||
def run
|
||||
Thread.current[THREAD_KEY] = self
|
||||
|
||||
@started_at = Time.now
|
||||
|
||||
yield
|
||||
ensure
|
||||
@finished_at = Time.now
|
||||
|
||||
Thread.current[THREAD_KEY] = nil
|
||||
end
|
||||
|
||||
def add_metric(series, values, tags = {})
|
||||
tags = tags.merge(transaction_id: @uuid)
|
||||
|
||||
@metrics << Metric.new(series, values, tags)
|
||||
end
|
||||
|
||||
def add_tag(key, value)
|
||||
@tags[key] = value
|
||||
end
|
||||
|
||||
def finish
|
||||
track_self
|
||||
submit
|
||||
end
|
||||
|
||||
def track_self
|
||||
add_metric(SERIES, { duration: duration }, @tags)
|
||||
end
|
||||
|
||||
def submit
|
||||
Metrics.submit_metrics(@metrics.map(&:to_hash))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
module Gitlab
|
||||
module Recaptcha
|
||||
def self.load_configurations!
|
||||
if current_application_settings.recaptcha_enabled
|
||||
::Recaptcha.configure do |config|
|
||||
config.public_key = current_application_settings.recaptcha_site_key
|
||||
config.private_key = current_application_settings.recaptcha_private_key
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,11 +3,12 @@ require 'banzai'
|
|||
module Gitlab
|
||||
# Extract possible GFM references from an arbitrary String for further processing.
|
||||
class ReferenceExtractor < Banzai::ReferenceExtractor
|
||||
attr_accessor :project, :current_user
|
||||
attr_accessor :project, :current_user, :author
|
||||
|
||||
def initialize(project, current_user = nil)
|
||||
def initialize(project, current_user = nil, author = nil)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@author = author
|
||||
|
||||
@references = {}
|
||||
|
||||
|
@ -20,18 +21,22 @@ module Gitlab
|
|||
|
||||
%i(user label merge_request snippet commit commit_range).each do |type|
|
||||
define_method("#{type}s") do
|
||||
@references[type] ||= references(type, project: project, current_user: current_user)
|
||||
@references[type] ||= references(type, reference_context)
|
||||
end
|
||||
end
|
||||
|
||||
def issues
|
||||
options = { project: project, current_user: current_user }
|
||||
|
||||
if project && project.jira_tracker?
|
||||
@references[:external_issue] ||= references(:external_issue, options)
|
||||
@references[:external_issue] ||= references(:external_issue, reference_context)
|
||||
else
|
||||
@references[:issue] ||= references(:issue, options)
|
||||
@references[:issue] ||= references(:issue, reference_context)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reference_context
|
||||
{ project: project, current_user: current_user, author: author }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,4 +98,56 @@ feature 'Login', feature: true do
|
|||
expect(page).to have_content('Invalid login or password.')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with required two-factor authentication enabled' do
|
||||
let(:user) { create(:user) }
|
||||
before(:each) { stub_application_setting(require_two_factor_authentication: true) }
|
||||
|
||||
context 'with grace period defined' do
|
||||
before(:each) do
|
||||
stub_application_setting(two_factor_grace_period: 48)
|
||||
login_with(user)
|
||||
end
|
||||
|
||||
context 'within the grace period' do
|
||||
it 'redirects to two-factor configuration page' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must configure Two-Factor Authentication in your account until')
|
||||
end
|
||||
|
||||
it 'two-factor configuration is skippable' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
|
||||
click_link 'Configure it later'
|
||||
expect(current_path).to eq root_path
|
||||
end
|
||||
end
|
||||
|
||||
context 'after the grace period' do
|
||||
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
|
||||
|
||||
it 'redirects to two-factor configuration page' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
|
||||
end
|
||||
|
||||
it 'two-factor configuration is not skippable' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).not_to have_link('Configure it later')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without grace pariod defined' do
|
||||
before(:each) do
|
||||
stub_application_setting(two_factor_grace_period: 0)
|
||||
login_with(user)
|
||||
end
|
||||
|
||||
it 'redirects to two-factor configuration page' do
|
||||
expect(current_path).to eq new_profile_two_factor_auth_path
|
||||
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -141,4 +141,11 @@ describe IssuesHelper do
|
|||
expect(note_active_class(Note.all, @note.author)).to eq("active")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#awards_sort" do
|
||||
it "sorts a hash so thumbsup and thumbsdown are always on top" do
|
||||
data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
|
||||
expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue