Commit 69dad967 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into refactor/ci-config-move-job-entries

* master: (183 commits)
  Add a spec for #20079.
  Skip repository storage path valitaions on test environment
  Use Pathname to make the repository storage path validations more robust
  Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
  Change nav link snippet controller
  Reduce min width of pipeline table
  Retrieve rendered HTML from cache in one request
  Explain CI_PROJECT_NAMESPACE better
  Bump vmstat version to fix issues reporting on FreeBSD
  Fix sha icon positioning on safari
  Don't drop in DropAndReaddHasExternalWikiInProjects
  Mobile view for commit status
  Fix ci icons getting cut off
  Update CHANGELOG
  Extract helper methods to clean up RepositoryArchiveCleanUpService spec
  Use Dir.mktmpdir instead of FileUtils.mkdir_p in the spec
  Fix firefox rendering of SVGs
  Fix icons on commits page and builds page
  Add new fork SVG to fix weird styling of other SVGs
  Bug fixes
  ...
parents dff10976 cbe787c5
image: "ruby:2.1" image: "ruby:2.1"
services:
- mysql:latest
- redis:alpine
cache: cache:
key: "ruby21" key: "ruby21"
paths: paths:
...@@ -34,7 +30,6 @@ stages: ...@@ -34,7 +30,6 @@ stages:
- post-test - post-test
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
.knapsack-state: &knapsack-state .knapsack-state: &knapsack-state
services: [] services: []
variables: variables:
...@@ -68,8 +63,14 @@ update-knapsack: ...@@ -68,8 +63,14 @@ update-knapsack:
# Execute all testing suites # Execute all testing suites
.use-db: &use-db
services:
- mysql:latest
- redis:alpine
.rspec-knapsack: &rspec-knapsack .rspec-knapsack: &rspec-knapsack
stage: test stage: test
<<: *use-db
script: script:
- bundle exec rake assets:precompile 2>/dev/null - bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME ) - JOB_NAME=( $CI_BUILD_NAME )
...@@ -85,6 +86,7 @@ update-knapsack: ...@@ -85,6 +86,7 @@ update-knapsack:
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
<<: *use-db
script: script:
- bundle exec rake assets:precompile 2>/dev/null - bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME ) - JOB_NAME=( $CI_BUILD_NAME )
...@@ -133,6 +135,7 @@ spinach 9 10: *spinach-knapsack ...@@ -133,6 +135,7 @@ spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.3 # Execute all testing suites against Ruby 2.3
.ruby-23: &ruby-23 .ruby-23: &ruby-23
image: "ruby:2.3" image: "ruby:2.3"
<<: *use-db
only: only:
- master - master
cache: cache:
...@@ -148,7 +151,7 @@ spinach 9 10: *spinach-knapsack ...@@ -148,7 +151,7 @@ spinach 9 10: *spinach-knapsack
.spinach-knapsack-ruby23: &spinach-knapsack-ruby23 .spinach-knapsack-ruby23: &spinach-knapsack-ruby23
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *ruby-23 <<: *ruby-23
rspec 0 20 ruby23: *rspec-knapsack-ruby23 rspec 0 20 ruby23: *rspec-knapsack-ruby23
rspec 1 20 ruby23: *rspec-knapsack-ruby23 rspec 1 20 ruby23: *rspec-knapsack-ruby23
rspec 2 20 ruby23: *rspec-knapsack-ruby23 rspec 2 20 ruby23: *rspec-knapsack-ruby23
...@@ -183,22 +186,41 @@ spinach 9 10 ruby23: *spinach-knapsack-ruby23 ...@@ -183,22 +186,41 @@ spinach 9 10 ruby23: *spinach-knapsack-ruby23
# Other generic tests # Other generic tests
.static-analyses-variables: &static-analyses-variables
variables:
SIMPLECOV: "false"
USE_DB: "false"
USE_BUNDLE_INSTALL: "true"
.exec: &exec .exec: &exec
<<: *static-analyses-variables
stage: test stage: test
script: script:
- bundle exec $CI_BUILD_NAME - bundle exec $CI_BUILD_NAME
teaspoon: *exec
rubocop: *exec rubocop: *exec
rake scss_lint: *exec rake scss_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flog: *exec rake flog: *exec
rake flay: *exec rake flay: *exec
rake db:migrate:reset: *exec
license_finder: *exec license_finder: *exec
rake downtime_check: *exec
rake db:migrate:reset:
stage: test
<<: *use-db
script:
- rake db:migrate:reset
teaspoon:
stage: test
<<: *use-db
script:
- teaspoon
bundler:audit: bundler:audit:
stage: test stage: test
<<: *static-analyses-variables
only: only:
- master - master
script: script:
......
...@@ -291,6 +291,10 @@ Style/MultilineMethodDefinitionBraceLayout: ...@@ -291,6 +291,10 @@ Style/MultilineMethodDefinitionBraceLayout:
Style/MultilineOperationIndentation: Style/MultilineOperationIndentation:
Enabled: false Enabled: false
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
Enabled: true
# Favor unless over if for negative conditions (or control flow or). # Favor unless over if for negative conditions (or control flow or).
Style/NegatedIf: Style/NegatedIf:
Enabled: true Enabled: true
......
...@@ -226,10 +226,6 @@ Style/LineEndConcatenation: ...@@ -226,10 +226,6 @@ Style/LineEndConcatenation:
Style/MethodCallParentheses: Style/MethodCallParentheses:
Enabled: false Enabled: false
# Offense count: 3
Style/MultilineTernaryOperator:
Enabled: false
# Offense count: 62 # Offense count: 62
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/MutableConstant: Style/MutableConstant:
......
This diff is collapsed.
...@@ -52,7 +52,7 @@ gem 'browser', '~> 2.2' ...@@ -52,7 +52,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.3.2' gem 'gitlab_git', '~> 10.4.1'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
...@@ -347,8 +347,5 @@ gem 'paranoia', '~> 2.0' ...@@ -347,8 +347,5 @@ gem 'paranoia', '~> 2.0'
gem 'health_check', '~> 2.1.0' gem 'health_check', '~> 2.1.0'
# System information # System information
gem 'vmstat', '~> 2.1.0' gem 'vmstat', '~> 2.1.1'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Secure headers for Content Security Policy
gem 'secure_headers', '~> 3.3'
...@@ -274,7 +274,7 @@ GEM ...@@ -274,7 +274,7 @@ GEM
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_git (10.3.2) gitlab_git (10.4.1)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -645,8 +645,6 @@ GEM ...@@ -645,8 +645,6 @@ GEM
sdoc (0.3.20) sdoc (0.3.20)
json (>= 1.1.3) json (>= 1.1.3)
rdoc (~> 3.10) rdoc (~> 3.10)
secure_headers (3.3.2)
useragent
seed-fu (2.3.6) seed-fu (2.3.6)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
...@@ -769,7 +767,6 @@ GEM ...@@ -769,7 +767,6 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.9.0) uniform_notifier (1.9.0)
useragent (0.16.7)
uuid (2.3.8) uuid (2.3.8)
macaddr (~> 1.0) macaddr (~> 1.0)
version_sorter (2.0.0) version_sorter (2.0.0)
...@@ -778,7 +775,7 @@ GEM ...@@ -778,7 +775,7 @@ GEM
coercible (~> 1.0) coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
vmstat (2.1.0) vmstat (2.1.1)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
web-console (2.3.0) web-console (2.3.0)
...@@ -864,7 +861,7 @@ DEPENDENCIES ...@@ -864,7 +861,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
github-markup (~> 1.4) github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.3.2) gitlab_git (~> 10.4.1)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
...@@ -947,7 +944,6 @@ DEPENDENCIES ...@@ -947,7 +944,6 @@ DEPENDENCIES
sass-rails (~> 5.0.0) sass-rails (~> 5.0.0)
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
sdoc (~> 0.3.20) sdoc (~> 0.3.20)
secure_headers (~> 3.3)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 1.1.0) sentry-raven (~> 1.1.0)
...@@ -984,7 +980,7 @@ DEPENDENCIES ...@@ -984,7 +980,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.2) unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.0.0) version_sorter (~> 2.0.0)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.1.0) vmstat (~> 2.1.1)
web-console (~> 2.0) web-console (~> 2.0)
webmock (~> 1.21.0) webmock (~> 1.21.0)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
......
...@@ -38,3 +38,14 @@ class @Admin ...@@ -38,3 +38,14 @@ class @Admin
$('li.group_member').bind 'ajax:success', -> $('li.group_member').bind 'ajax:success', ->
Turbolinks.visit(location.href) Turbolinks.visit(location.href)
showBlacklistType = ->
if $("input[name='blacklist_type']:checked").val() == 'file'
$('.blacklist-file').show()
$('.blacklist-raw').hide()
else
$('.blacklist-file').hide()
$('.blacklist-raw').show()
$("input[name='blacklist_type']").click showBlacklistType
showBlacklistType()
...@@ -7,7 +7,6 @@ class @FilesCommentButton ...@@ -7,7 +7,6 @@ class @FilesCommentButton
UNFOLDABLE_LINE_CLASS = 'js-unfold' UNFOLDABLE_LINE_CLASS = 'js-unfold'
EMPTY_CELL_CLASS = 'empty-cell' EMPTY_CELL_CLASS = 'empty-cell'
OLD_LINE_CLASS = 'old_line' OLD_LINE_CLASS = 'old_line'
NEW_CLASS = 'new'
LINE_COLUMN_CLASSES = ".#{LINE_NUMBER_CLASS}, .line_content" LINE_COLUMN_CLASSES = ".#{LINE_NUMBER_CLASS}, .line_content"
TEXT_FILE_SELECTOR = '.text-file' TEXT_FILE_SELECTOR = '.text-file'
DEBOUNCE_TIMEOUT_DURATION = 100 DEBOUNCE_TIMEOUT_DURATION = 100
...@@ -18,6 +17,8 @@ class @FilesCommentButton ...@@ -18,6 +17,8 @@ class @FilesCommentButton
debounce = _.debounce @render, DEBOUNCE_TIMEOUT_DURATION debounce = _.debounce @render, DEBOUNCE_TIMEOUT_DURATION
$(document) $(document)
.off 'mouseover', LINE_COLUMN_CLASSES
.off 'mouseleave', LINE_COLUMN_CLASSES
.on 'mouseover', LINE_COLUMN_CLASSES, debounce .on 'mouseover', LINE_COLUMN_CLASSES, debounce
.on 'mouseleave', LINE_COLUMN_CLASSES, @destroy .on 'mouseleave', LINE_COLUMN_CLASSES, @destroy
...@@ -64,20 +65,20 @@ class @FilesCommentButton ...@@ -64,20 +65,20 @@ class @FilesCommentButton
getLineContent: (hoveredElement) -> getLineContent: (hoveredElement) ->
return hoveredElement if hoveredElement.hasClass LINE_CONTENT_CLASS return hoveredElement if hoveredElement.hasClass LINE_CONTENT_CLASS
$(".#{LINE_CONTENT_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent()) if @VIEW_TYPE is 'inline'
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find ".#{LINE_CONTENT_CLASS}"
else
return $(hoveredElement).next ".#{LINE_CONTENT_CLASS}"
getButtonParent: (hoveredElement) -> getButtonParent: (hoveredElement) ->
if @VIEW_TYPE is 'inline' if @VIEW_TYPE is 'inline'
return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS
$(".#{OLD_LINE_CLASS}", hoveredElement.parent()) hoveredElement.parent().find ".#{OLD_LINE_CLASS}"
else else
return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS
$(".#{LINE_NUMBER_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent()) $(hoveredElement).prev ".#{LINE_NUMBER_CLASS}"
diffTypeClass: (hoveredElement) ->
if hoveredElement.hasClass(NEW_CLASS) then '.new' else '.old'
isMovingToSameType: (e) -> isMovingToSameType: (e) ->
newButtonParent = @getButtonParent $(e.toElement) newButtonParent = @getButtonParent $(e.toElement)
......
...@@ -210,9 +210,22 @@ class GitLabDropdown ...@@ -210,9 +210,22 @@ class GitLabDropdown
data: => data: =>
return @fullData return @fullData
callback: (data) => callback: (data) =>
currentIndex = -1
@parseData data @parseData data
unless @filterInput.val() is ''
selector = '.dropdown-content li:not(.divider):visible'
if @dropdown.find('.dropdown-toggle-page').length
selector = ".dropdown-page-one #{selector}"
$(selector, @dropdown)
.first()
.find('a')
.addClass('is-focused')
currentIndex = 0
# Event listeners # Event listeners
@dropdown.on "shown.bs.dropdown", @opened @dropdown.on "shown.bs.dropdown", @opened
......
...@@ -55,10 +55,13 @@ class @MergeRequestWidget ...@@ -55,10 +55,13 @@ class @MergeRequestWidget
$('.mr-state-widget').replaceWith(data) $('.mr-state-widget').replaceWith(data)
ciLabelForStatus: (status) -> ciLabelForStatus: (status) ->
if status is 'success' switch status
'passed' when 'success'
else 'passed'
status when 'success_with_warnings'
'passed with warnings'
else
status
pollCIStatus: -> pollCIStatus: ->
@fetchBuildStatusInterval = setInterval ( => @fetchBuildStatusInterval = setInterval ( =>
...@@ -116,7 +119,7 @@ class @MergeRequestWidget ...@@ -116,7 +119,7 @@ class @MergeRequestWidget
showCIStatus: (state) -> showCIStatus: (state) ->
return if not state? return if not state?
$('.ci_widget').hide() $('.ci_widget').hide()
allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]
if state in allowed_states if state in allowed_states
$('.ci_widget.ci-' + state).show() $('.ci_widget.ci-' + state).show()
switch state switch state
...@@ -124,7 +127,7 @@ class @MergeRequestWidget ...@@ -124,7 +127,7 @@ class @MergeRequestWidget
@setMergeButtonClass('btn-danger') @setMergeButtonClass('btn-danger')
when "running" when "running"
@setMergeButtonClass('btn-warning') @setMergeButtonClass('btn-warning')
when "success" when "success", "success_with_warnings"
@setMergeButtonClass('btn-create') @setMergeButtonClass('btn-create')
else else
$('.ci_widget.ci-error').show() $('.ci_widget.ci-error').show()
......
...@@ -162,7 +162,7 @@ class @Notes ...@@ -162,7 +162,7 @@ class @Notes
@last_fetched_at = data.last_fetched_at @last_fetched_at = data.last_fetched_at
@setPollingInterval(data.notes.length) @setPollingInterval(data.notes.length)
$.each notes, (i, note) => $.each notes, (i, note) =>
if note.discussion_with_diff_html? if note.discussion_html?
@renderDiscussionNote(note) @renderDiscussionNote(note)
else else
@renderNote(note) @renderNote(note)
...@@ -251,7 +251,7 @@ class @Notes ...@@ -251,7 +251,7 @@ class @Notes
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']") discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']")
if discussionContainer.length is 0 if discussionContainer.length is 0
# insert the note and the reply button after the temp row # insert the note and the reply button after the temp row
row.after note.discussion_html row.after note.diff_discussion_html
# remove the note (will be added again below) # remove the note (will be added again below)
row.next().find(".note").remove() row.next().find(".note").remove()
...@@ -265,7 +265,7 @@ class @Notes ...@@ -265,7 +265,7 @@ class @Notes
# Init discussion on 'Discussion' page if it is merge request page # Init discussion on 'Discussion' page if it is merge request page
if $('body').attr('data-page').indexOf('projects:merge_request') is 0 if $('body').attr('data-page').indexOf('projects:merge_request') is 0
$('ul.main-notes-list') $('ul.main-notes-list')
.append(note.discussion_with_diff_html) .append(note.discussion_html)
.syntaxHighlight() .syntaxHighlight()
else else
# append new note to all matching discussions # append new note to all matching discussions
......
...@@ -120,6 +120,9 @@ class @Sidebar ...@@ -120,6 +120,9 @@ class @Sidebar
i.show() i.show()
sidebarCollapseClicked: (e) -> sidebarCollapseClicked: (e) ->
return if $(e.currentTarget).hasClass('dont-change-state')
sidebar = e.data sidebar = e.data
e.preventDefault() e.preventDefault()
$block = $(@).closest('.block') $block = $(@).closest('.block')
......
...@@ -232,7 +232,9 @@ ...@@ -232,7 +232,9 @@
.nav-block { .nav-block {
.controls { .controls {
float: right; float: right;
margin-top: 11px; margin-top: 8px;
padding-bottom: 7px;
border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -98,13 +98,30 @@ ...@@ -98,13 +98,30 @@
.md { .md {
&.md-preview-holder { &.md-preview-holder {
code { // Reset ul style types since we're nested inside a ul already
white-space: pre-wrap;
word-break: keep-all;
}
@include bulleted-list; @include bulleted-list;
} }
// On diffs code should wrap nicely and not overflow
code {
white-space: pre-wrap;
word-break: keep-all;
}
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
border-color: darken(#f5f5f5, 8%);
margin: 10px 0;
}
// Border around images in issue and MR comments.
img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px 0;
// Ensure that image does not exceed viewport
max-height: calc(100vh - 100px);
}
} }
.toolbar-group { .toolbar-group {
......
...@@ -53,6 +53,14 @@ ...@@ -53,6 +53,14 @@
left: 70px; left: 70px;
} }
} }
.nav-links {
svg {
position: relative;
top: 2px;
margin-right: 3px;
}
}
} }
.build-header { .build-header {
......
...@@ -68,6 +68,12 @@ ...@@ -68,6 +68,12 @@
} }
} }
.ci-status-link {
svg {
overflow: visible;
}
}
.commit-box { .commit-box {
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
......
...@@ -176,3 +176,11 @@ ...@@ -176,3 +176,11 @@
} }
} }
} }
// hide event scope (namespace + project) where it is not necessary
.project-activity {
.event-scope {
display: none;
}
}
...@@ -269,7 +269,7 @@ ...@@ -269,7 +269,7 @@
.issuable-header-btn { .issuable-header-btn {
background: $gray-normal; background: $gray-normal;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
&:hover { &:hover {
background: $gray-dark; background: $gray-dark;
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
......
...@@ -64,12 +64,21 @@ ...@@ -64,12 +64,21 @@
margin-right: 4px; margin-right: 4px;
position: relative; position: relative;
top: 1px; top: 1px;
overflow: visible;
} }
&.ci-success { &.ci-success {
color: $gl-success; color: $gl-success;
} }
&.ci-success_with_warnings {
color: $gl-success;
i {
color: $gl-warning;
}
}
&.ci-skipped { &.ci-skipped {
background-color: #eee; background-color: #eee;
color: #888; color: #888;
......
...@@ -91,34 +91,11 @@ ul.notes { ...@@ -91,34 +91,11 @@ ul.notes {
// Reset ul style types since we're nested inside a ul already // Reset ul style types since we're nested inside a ul already
@include bulleted-list; @include bulleted-list;
// On diffs code should wrap nicely and not overflow
code {
white-space: pre-wrap;
}
ul.task-list { ul.task-list {
ul:not(.task-list) { ul:not(.task-list) {
padding-left: 1.3em; padding-left: 1.3em;
} }
} }
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
border-color: darken(#f5f5f5, 8%);
margin: 10px 0;
}
code {
word-break: keep-all;
}
// Border around images in issue and MR comments.
img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px 0;
max-height: calc(100vh - 100px);
}
} }
} }
......
...@@ -29,9 +29,18 @@ ...@@ -29,9 +29,18 @@
} }
} }
.pipeline-holder {
width: 100%;
overflow: auto;
}
.table.builds { .table.builds {
min-width: 1200px; min-width: 1200px;
&.pipeline {
min-width: 650px;
}
tr { tr {
th { th {
padding: 16px 8px; padding: 16px 8px;
...@@ -76,7 +85,7 @@ ...@@ -76,7 +85,7 @@
svg { svg {
height: 14px; height: 14px;
width: auto; width: 14px;
vertical-align: middle; vertical-align: middle;
fill: $table-text-gray; fill: $table-text-gray;
} }
...@@ -93,7 +102,7 @@ ...@@ -93,7 +102,7 @@
.commit-title { .commit-title {
margin-top: 4px; margin-top: 4px;
max-width: 320px; max-width: 300px;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -138,6 +147,11 @@ ...@@ -138,6 +147,11 @@
height: 18px; height: 18px;
width: 18px; width: 18px;
vertical-align: middle; vertical-align: middle;
overflow: visible;
}
.light {
width: 3px;
} }
} }
...@@ -153,7 +167,7 @@ ...@@ -153,7 +167,7 @@
svg { svg {
width: 12px; width: 12px;
height: auto; height: 12px;
vertical-align: middle; vertical-align: middle;
margin-right: 4px; margin-right: 4px;
} }
......
...@@ -333,18 +333,53 @@ a.deploy-project-label { ...@@ -333,18 +333,53 @@ a.deploy-project-label {
} }
.fork-namespaces { .fork-namespaces {
.fork-thumbnail { .row {
text-align: center; -webkit-flex-wrap: wrap;
margin-bottom: $gl-padding; display: -webkit-flex;
display: flex;
.caption { flex-wrap: wrap;
padding: $gl-padding 0; justify-content: flex-start;
min-height: 30px;
} .fork-thumbnail {
@include border-radius($border-radius-base);
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
margin: $gl-padding;
text-align: center;
width: 169px;
&:hover, &.forked {
background-color: $row-hover;
border-color: $row-hover-border;
}
.no-avatar {
width: 100px;
height: 100px;
background-color: $gray-light;
border: 1px solid $gray-dark;
margin: 0 auto;
@include border-radius(50%);
i {
font-size: 100px;
color: $gray-dark;
}
}
a {
display: block;
width: 100%;
height: 100%;
padding-top: $gl-padding;
color: $gl-gray;
.caption {
min-height: 30px;
padding: $gl-padding 0;
}
}
img { img {
@include border-radius(50%); @include border-radius(50%);
max-width: 100px; max-width: 100px;
}
} }
} }
} }
......
...@@ -15,7 +15,8 @@ ...@@ -15,7 +15,8 @@
border-color: $gl-danger; border-color: $gl-danger;
} }
&.ci-success { &.ci-success,
&.ci-success_with_warnings {
color: $gl-success; color: $gl-success;
border-color: $gl-success; border-color: $gl-success;
} }
...@@ -48,6 +49,7 @@ ...@@ -48,6 +49,7 @@
position: relative; position: relative;
top: 1px; top: 1px;
margin: 0 3px; margin: 0 3px;
overflow: visible;
} }
} }
...@@ -57,9 +59,12 @@ ...@@ -57,9 +59,12 @@
.ci-status-icon-failed { .ci-status-icon-failed {
color: $gl-danger; color: $gl-danger;
} }
.ci-status-icon-pending {
.ci-status-icon-pending,
.ci-status-icon-success_with_warning {
color: $gl-warning; color: $gl-warning;
} }
.ci-status-icon-running { .ci-status-icon-running {
color: $blue-normal; color: $blue-normal;
} }
...@@ -70,3 +75,11 @@ ...@@ -70,3 +75,11 @@
color: $gl-gray; color: $gl-gray;
} }
} }
.visible-xs-inline {
.ci-status-link {
position: relative;
top: 2px;
left: 5px;
}
}
...@@ -64,6 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -64,6 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:disabled_oauth_sign_in_sources] = params[:application_setting][:disabled_oauth_sign_in_sources] =
AuthHelper.button_based_providers.map(&:to_s) - AuthHelper.button_based_providers.map(&:to_s) -
Array(enabled_oauth_sign_in_sources) Array(enabled_oauth_sign_in_sources)
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit( params.require(:application_setting).permit(
:default_projects_limit, :default_projects_limit,
...@@ -83,7 +84,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -83,7 +84,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_project_visibility, :default_project_visibility,
:default_snippet_visibility, :default_snippet_visibility,
:default_group_visibility, :default_group_visibility,
:restricted_signup_domains_raw, :domain_whitelist_raw,
:domain_blacklist_enabled,
:domain_blacklist_raw,
:domain_blacklist_file,
:version_check_enabled, :version_check_enabled,
:admin_notification_email, :admin_notification_email,
:user_oauth_applications, :user_oauth_applications,
......
...@@ -60,6 +60,6 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -60,6 +60,6 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level) params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled)
end end
end end
class Admin::ServicesController < Admin::ApplicationController class Admin::ServicesController < Admin::ApplicationController
include ServiceParams
before_action :service, only: [:edit, :update] before_action :service, only: [:edit, :update]
def index def index
...@@ -13,7 +15,7 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -13,7 +15,7 @@ class Admin::ServicesController < Admin::ApplicationController
end end
def update def update
if service.update_attributes(application_services_params[:service]) if service.update_attributes(service_params[:service])
redirect_to admin_application_settings_services_path, redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully' notice: 'Application settings saved successfully'
else else
...@@ -37,15 +39,4 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -37,15 +39,4 @@ class Admin::ServicesController < Admin::ApplicationController
def service def service
@service ||= Service.where(id: params[:id], template: true).first @service ||= Service.where(id: params[:id], template: true).first
end end
def application_services_params
application_services_params = params.permit(:id,
service: Projects::ServicesController::ALLOWED_PARAMS)
if application_services_params[:service].is_a?(Hash)
Projects::ServicesController::FILTER_BLANK_PARAMS.each do |param|
application_services_params[:service].delete(param) if application_services_params[:service][param].blank?
end
end
application_services_params
end
end end
module ServiceParams
extend ActiveSupport::Concern
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
:note_events, :build_events, :wiki_page_events,
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
def service_params
dynamic_params = []
dynamic_params.concat(@service.event_channel_names)
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param|
service_params[:service].delete(param) if service_params[:service][param].blank?
end
end
service_params
end
end
...@@ -121,7 +121,7 @@ class GroupsController < Groups::ApplicationController ...@@ -121,7 +121,7 @@ class GroupsController < Groups::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock) params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled)
end end
def load_events def load_events
......
...@@ -30,7 +30,7 @@ class HelpController < ApplicationController ...@@ -30,7 +30,7 @@ class HelpController < ApplicationController
end end
# Allow access to images in the doc folder # Allow access to images in the doc folder
format.any(:png, :gif, :jpeg) do format.any(:png, :gif, :jpeg, :mp4) do
# Note: We are purposefully NOT using `Rails.root.join` # Note: We are purposefully NOT using `Rails.root.join`
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}") path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
......
...@@ -3,11 +3,6 @@ class Projects::BadgesController < Projects::ApplicationController ...@@ -3,11 +3,6 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:index] before_action :authorize_admin_project!, only: [:index]
before_action :no_cache_headers, except: [:index] before_action :no_cache_headers, except: [:index]
def index
@ref = params[:ref] || @project.default_branch || 'master'
@build_badge = Gitlab::Badge::Build.new(@project, @ref)
end
def build def build
badge = Gitlab::Badge::Build.new(project, params[:ref]) badge = Gitlab::Badge::Build.new(project, params[:ref])
......
...@@ -115,11 +115,11 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -115,11 +115,11 @@ class Projects::CommitController < Projects::ApplicationController
end end
def define_note_vars def define_note_vars
@grouped_diff_notes = commit.notes.grouped_diff_notes @grouped_diff_discussions = commit.notes.grouped_diff_discussions
@notes = commit.notes.non_diff_notes.fresh @notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render( Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten + @notes, @grouped_diff_discussions.values.flat_map(&:notes) + @notes,
@project, @project,
current_user, current_user,
) )
......
...@@ -15,6 +15,7 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -15,6 +15,7 @@ class Projects::CompareController < Projects::ApplicationController
end end
def show def show
apply_diff_view_cookie!
end end
def diff_for_path def diff_for_path
...@@ -53,7 +54,7 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -53,7 +54,7 @@ class Projects::CompareController < Projects::ApplicationController
) )
@diff_notes_disabled = true @diff_notes_disabled = true
@grouped_diff_notes = {} @grouped_diff_discussions = {}
end end
end end
......
...@@ -97,7 +97,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -97,7 +97,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else else
build_merge_request build_merge_request
@diff_notes_disabled = true @diff_notes_disabled = true
@grouped_diff_notes = {} @grouped_diff_discussions = {}
end end
define_commit_vars define_commit_vars
...@@ -286,6 +286,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -286,6 +286,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
status = pipeline.status status = pipeline.status
coverage = pipeline.try(:coverage) coverage = pipeline.try(:coverage)
status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
status ||= "preparing" status ||= "preparing"
else else
ci_service = @merge_request.source_project.ci_service ci_service = @merge_request.source_project.ci_service
...@@ -376,7 +378,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -376,7 +378,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# This is not executed lazily # This is not executed lazily
@notes = Banzai::NoteRenderer.render( @notes = Banzai::NoteRenderer.render(
@discussions.flatten, @discussions.flat_map(&:notes),
@project, @project,
current_user, current_user,
@path, @path,
...@@ -402,10 +404,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -402,10 +404,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
} }
@use_legacy_diff_notes = !@merge_request.support_new_diff_notes? @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
@grouped_diff_notes = @merge_request.notes.grouped_diff_notes @grouped_diff_discussions = @merge_request.notes.grouped_diff_discussions
Banzai::NoteRenderer.render( Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten, @grouped_diff_discussions.values.flat_map(&:notes),
@project, @project,
current_user, current_user,
@path, @path,
......
...@@ -73,7 +73,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -73,7 +73,7 @@ class Projects::NotesController < Projects::ApplicationController
end end
alias_method :awardable, :note alias_method :awardable, :note
def note_to_html(note) def note_html(note)
render_to_string( render_to_string(
"projects/notes/_note", "projects/notes/_note",
layout: false, layout: false,
...@@ -82,20 +82,20 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -82,20 +82,20 @@ class Projects::NotesController < Projects::ApplicationController
) )
end end
def note_to_discussion_html(note) def diff_discussion_html(discussion)
return unless note.diff_note? return unless discussion.diff_discussion?
if params[:view] == 'parallel' if params[:view] == 'parallel'
template = "projects/notes/_diff_notes_with_reply_parallel" template = "discussions/_parallel_diff_discussion"
locals = locals =
if params[:line_type] == 'old' if params[:line_type] == 'old'
{ notes_left: [note], notes_right: [] } { discussion_left: discussion, discussion_right: nil }
else else
{ notes_left: [], notes_right: [note] } { discussion_left: nil, discussion_right: discussion }
end end
else else
template = "projects/notes/_diff_notes_with_reply" template = "discussions/_diff_discussion"
locals = { notes: [note] } locals = { discussion: discussion }
end end
render_to_string( render_to_string(
...@@ -106,14 +106,14 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -106,14 +106,14 @@ class Projects::NotesController < Projects::ApplicationController
) )
end end
def note_to_discussion_with_diff_html(note) def discussion_html(discussion)
return unless note.diff_note? return unless discussion.diff_discussion?
render_to_string( render_to_string(
"projects/notes/_discussion", "discussions/_discussion",
layout: false, layout: false,
formats: [:html], formats: [:html],
locals: { discussion_notes: [note] } locals: { discussion: discussion }
) )
end end
...@@ -132,26 +132,33 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -132,26 +132,33 @@ class Projects::NotesController < Projects::ApplicationController
valid: true, valid: true,
id: note.id, id: note.id,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_to_html(note), html: note_html(note),
award: false, award: false,
note: note.note, note: note.note
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
} }
# The discussion_id is used to add the comment to the correct discussion if note.diff_note?
# element on the merge request page. Among other things, the discussion_id discussion = Discussion.new([note])
# contains the sha of head commit of the merge request.
# When new commits are pushed into the merge request after the initial attrs.merge!(
# load of the merge request page, the discussion elements will still have diff_discussion_html: diff_discussion_html(discussion),
# the old discussion_ids, with the old head commit sha. The new comment, discussion_html: discussion_html(discussion)
# however, will have the new discussion_id with the new commit sha. )
# To ensure that these new comments will still end up in the correct
# discussion element, we also send the original discussion_id, with the # The discussion_id is used to add the comment to the correct discussion
# old commit sha, along, and fall back on this value when no discussion # element on the merge request page. Among other things, the discussion_id
# element with the new discussion_id could be found. # contains the sha of head commit of the merge request.
if note.new_diff_note? && note.position != note.original_position # When new commits are pushed into the merge request after the initial
attrs[:original_discussion_id] = note.original_discussion_id # load of the merge request page, the discussion elements will still have
# the old discussion_ids, with the old head commit sha. The new comment,
# however, will have the new discussion_id with the new commit sha.
# To ensure that these new comments will still end up in the correct
# discussion element, we also send the original discussion_id, with the
# old commit sha, along, and fall back on this value when no discussion
# element with the new discussion_id could be found.
if note.new_diff_note? && note.position != note.original_position
attrs[:original_discussion_id] = note.original_discussion_id
end
end end
attrs attrs
......
class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
@ref = params[:ref] || @project.default_branch || 'master'
@build_badge = Gitlab::Badge::Build.new(@project, @ref)
end
def update
if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
else
render 'index'
end
end
private
def create_params
params.require(:pipeline).permit(:ref)
end
def update_params
params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds
)
end
end
...@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController
when "graphs_commits" when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id) commits_namespace_project_graph_path(@project.namespace, @project, @id)
when "badges" when "badges"
namespace_project_badges_path(@project.namespace, @project, ref: @id) namespace_project_pipelines_settings_path(@project.namespace, @project, ref: @id)
else else
namespace_project_commits_path(@project.namespace, @project, @id) namespace_project_commits_path(@project.namespace, @project, @id)
end end
......
class Projects::ServicesController < Projects::ApplicationController class Projects::ServicesController < Projects::ApplicationController
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, include ServiceParams
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
:note_events, :build_events, :wiki_page_events,
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
...@@ -33,7 +18,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -33,7 +18,7 @@ class Projects::ServicesController < Projects::ApplicationController
end end
def update def update
if @service.update_attributes(service_params) if @service.update_attributes(service_params[:service])
redirect_to( redirect_to(
edit_namespace_project_service_path(@project.namespace, @project, edit_namespace_project_service_path(@project.namespace, @project,
@service.to_param, notice: @service.to_param, notice:
...@@ -64,12 +49,4 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -64,12 +49,4 @@ class Projects::ServicesController < Projects::ApplicationController
def service def service
@service ||= @project.services.find { |service| service.to_param == params[:id] } @service ||= @project.services.find { |service| service.to_param == params[:id] }
end end
def service_params
service_params = params.require(:service).permit(ALLOWED_PARAMS)
FILTER_BLANK_PARAMS.each do |param|
service_params.delete(param) if service_params[param].blank?
end
service_params
end
end end
class Projects::UploadsController < Projects::ApplicationController class Projects::UploadsController < Projects::ApplicationController
skip_before_action :reject_blocked!, :project, skip_before_action :reject_blocked!, :project,
:repository, if: -> { action_name == 'show' && image? } :repository, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create]
...@@ -24,7 +24,7 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -24,7 +24,7 @@ class Projects::UploadsController < Projects::ApplicationController
def show def show
return render_404 if uploader.nil? || !uploader.file.exists? return render_404 if uploader.nil? || !uploader.file.exists?
disposition = uploader.image? ? 'inline' : 'attachment' disposition = uploader.image_or_video? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition send_file uploader.file.path, disposition: disposition
end end
...@@ -49,7 +49,7 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -49,7 +49,7 @@ class Projects::UploadsController < Projects::ApplicationController
@uploader @uploader
end end
def image? def image_or_video?
uploader && uploader.file.exists? && uploader.image? uploader && uploader.file.exists? && uploader.image_or_video?
end end
end end
...@@ -296,7 +296,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -296,7 +296,7 @@ class ProjectsController < Projects::ApplicationController
:issues_tracker_id, :default_branch, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
) )
end end
......
module AvatarsHelper
def author_avatar(commit_or_event, options = {})
user_avatar(options.merge({
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
}))
end
private
def user_avatar(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}",
alt: "#{user_name}'s avatar",
title: user_name
)
if options[:user]
link_to(avatar, user_path(options[:user]))
elsif options[:user_email]
mail_to(options[:user_email], avatar)
end
end
end
...@@ -15,8 +15,11 @@ module CiStatusHelper ...@@ -15,8 +15,11 @@ module CiStatusHelper
end end
def ci_label_for_status(status) def ci_label_for_status(status)
if status == 'success' case status
when 'success'
'passed' 'passed'
when 'success_with_warnings'
'passed with warnings'
else else
status status
end end
...@@ -42,10 +45,10 @@ module CiStatusHelper ...@@ -42,10 +45,10 @@ module CiStatusHelper
custom_icon(icon_name) custom_icon(icon_name)
end end
def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '') def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit) path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass) render_status_with_link('commit', commit.status, path, tooltip_placement)
end end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left') def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
......
...@@ -16,16 +16,6 @@ module CommitsHelper ...@@ -16,16 +16,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer)) commit_person_link(commit, options.merge(source: :committer))
end end
def commit_author_avatar(commit, options = {})
options = options.merge(source: :author)
user = commit.send(options[:source])
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_email = user.try(:email) || source_email
image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
end
def image_diff_class(diff) def image_diff_class(diff)
if diff.deleted_file if diff.deleted_file
"deleted" "deleted"
......
...@@ -54,18 +54,20 @@ module DiffHelper ...@@ -54,18 +54,20 @@ module DiffHelper
end end
end end
def organize_comments(left, right) def parallel_diff_discussions(left, right, diff_file)
notes_left = notes_right = nil discussion_left = discussion_right = nil
unless left[:type].nil? && right[:type] == 'new' if left && (left.unchanged? || left.removed?)
notes_left = @grouped_diff_notes[left[:line_code]] line_code = diff_file.line_code(left)
discussion_left = @grouped_diff_discussions[line_code]
end end
unless left[:type].nil? && right[:type].nil? if right && right.added?
notes_right = @grouped_diff_notes[right[:line_code]] line_code = diff_file.line_code(right)
discussion_right = @grouped_diff_discussions[line_code]
end end
[notes_left, notes_right] [discussion_left, discussion_right]
end end
def inline_diff_btn def inline_diff_btn
......
module ExternalWikiHelper module ExternalWikiHelper
def get_project_wiki_path(project) def get_project_wiki_path(project)
external_wiki_service = project.services. external_wiki_service = project.external_wiki
find { |service| service.to_param == 'external_wiki' } if external_wiki_service
if external_wiki_service.present? && external_wiki_service.active?
external_wiki_service.properties['external_wiki_url'] external_wiki_service.properties['external_wiki_url']
else else
namespace_project_wiki_path(project.namespace, project, :home) namespace_project_wiki_path(project.namespace, project, :home)
......
module NotesHelper module NotesHelper
# Helps to distinguish e.g. commit notes in mr notes list
def note_for_main_target?(note)
@noteable.class.name == note.noteable_type && !note.diff_note?
end
def note_target_fields(note) def note_target_fields(note)
if note.noteable if note.noteable
hidden_field_tag(:target_type, note.noteable.class.name.underscore) + hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
...@@ -44,8 +39,8 @@ module NotesHelper ...@@ -44,8 +39,8 @@ module NotesHelper
# If we didn't, diff notes that would show for the same line on the changes # If we didn't, diff notes that would show for the same line on the changes
# tab, would show in different discussions on the discussion tab. # tab, would show in different discussions on the discussion tab.
use_legacy_diff_note ||= begin use_legacy_diff_note ||= begin
line_diff_notes = @grouped_diff_notes[line_code] discussion = @grouped_diff_discussions[line_code]
line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?) discussion && discussion.legacy_diff_discussion?
end end
data = { data = {
...@@ -81,22 +76,10 @@ module NotesHelper ...@@ -81,22 +76,10 @@ module NotesHelper
data data
end end
def link_to_reply_discussion(note, line_type = nil) def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user return unless current_user
data = { data = discussion.reply_attributes.merge(line_type: line_type)
noteable_type: note.noteable_type,
noteable_id: note.noteable_id,
commit_id: note.commit_id,
discussion_id: note.discussion_id,
line_type: line_type
}
if note.diff_note?
data[:note_type] = note.type
data.merge!(note.diff_attributes)
end
content_tag(:div, class: "discussion-reply-holder") do content_tag(:div, class: "discussion-reply-holder") do
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
...@@ -114,13 +97,13 @@ module NotesHelper ...@@ -114,13 +97,13 @@ module NotesHelper
@max_access_by_user_id[full_key] @max_access_by_user_id[full_key]
end end
def diff_note_path(note) def discussion_diff_path(discussion)
return unless note.diff_note? return unless discussion.diff_discussion?
if note.for_merge_request? && note.active? if discussion.for_merge_request? && discussion.active?
diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code)
elsif note.for_commit? elsif discussion.for_commit?
namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code)
end end
end end
end end
module ServicesHelper
def service_event_description(event)
case event
when "push"
"Event will be triggered by a push to the repository"
when "tag_push"
"Event will be triggered when a new tag is pushed to the repository"
when "note"
"Event will be triggered when someone adds a comment"
when "issue"
"Event will be triggered when an issue is created/updated/merged"
when "merge_request"
"Event will be triggered when a merge request is created/updated/merged"
when "build"
"Event will be triggered when a build status changes"
when "wiki_page"
"Event will be triggered when a wiki page is created/updated"
end
end
def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue].include?(event)
"#{event}_events"
end
end
module TimeHelper module TimeHelper
def time_interval_in_words(interval_in_seconds) def time_interval_in_words(interval_in_seconds)
interval_in_seconds = interval_in_seconds.to_i
minutes = interval_in_seconds / 60 minutes = interval_in_seconds / 60
seconds = interval_in_seconds - minutes * 60 seconds = interval_in_seconds - minutes * 60
......
...@@ -172,7 +172,7 @@ class Ability ...@@ -172,7 +172,7 @@ class Ability
rules << :read_build if project.public_builds? rules << :read_build if project.public_builds?
unless owner || project.team.member?(user) || project_group_member?(project, user) unless owner || project.team.member?(user) || project_group_member?(project, user)
rules << :request_access rules << :request_access if project.request_access_enabled
end end
end end
...@@ -373,7 +373,7 @@ class Ability ...@@ -373,7 +373,7 @@ class Ability
end end
if group.public? || (group.internal? && !user.external?) if group.public? || (group.internal? && !user.external?)
rules << :request_access unless group.users.include?(user) rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
end end
rules.flatten rules.flatten
......
...@@ -4,12 +4,20 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -4,12 +4,20 @@ class ApplicationSetting < ActiveRecord::Base
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
CACHE_KEY = 'application_setting.last' CACHE_KEY = 'application_setting.last'
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
}x
serialize :restricted_visibility_levels serialize :restricted_visibility_levels
serialize :import_sources serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array serialize :disabled_oauth_sign_in_sources, Array
serialize :restricted_signup_domains, Array serialize :domain_whitelist, Array
attr_accessor :restricted_signup_domains_raw serialize :domain_blacklist, Array
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay, validates :session_expire_delay,
presence: true, presence: true,
...@@ -62,6 +70,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -62,6 +70,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :enabled_git_access_protocol, validates :enabled_git_access_protocol,
inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true } inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
validates :domain_blacklist,
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil? unless value.nil?
value.each do |level| value.each do |level|
...@@ -129,7 +141,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -129,7 +141,7 @@ class ApplicationSetting < ActiveRecord::Base
session_expire_delay: Settings.gitlab['session_expire_delay'], session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project], import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
...@@ -150,20 +162,30 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -150,20 +162,30 @@ class ApplicationSetting < ActiveRecord::Base
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end end
def restricted_signup_domains_raw def domain_whitelist_raw
self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil? self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
end
def domain_blacklist_raw
self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
end
def domain_whitelist_raw=(values)
self.domain_whitelist = []
self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR)
self.domain_whitelist.reject! { |d| d.empty? }
self.domain_whitelist
end
def domain_blacklist_raw=(values)
self.domain_blacklist = []
self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR)
self.domain_blacklist.reject! { |d| d.empty? }
self.domain_blacklist
end end
def restricted_signup_domains_raw=(values) def domain_blacklist_file=(file)
self.restricted_signup_domains = [] self.domain_blacklist_raw = file.read
self.restricted_signup_domains = values.split(
/\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
/x)
self.restricted_signup_domains.reject! { |d| d.empty? }
end end
def runners_registration_token def runners_registration_token
......
...@@ -12,7 +12,7 @@ module Ci ...@@ -12,7 +12,7 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual) } scope :manual_actions, ->() { where(when: :manual) }
...@@ -97,7 +97,7 @@ module Ci ...@@ -97,7 +97,7 @@ module Ci
end end
def other_actions def other_actions
pipeline.manual_actions.where.not(id: self) pipeline.manual_actions.where.not(name: name)
end end
def playable? def playable?
...@@ -145,7 +145,15 @@ module Ci ...@@ -145,7 +145,15 @@ module Ci
end end
def variables def variables
predefined_variables + yaml_variables + project_variables + trigger_variables variables = predefined_variables
variables += project.predefined_variables
variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += yaml_variables
variables += project.secret_variables
variables += trigger_request.user_variables if trigger_request
variables
end end
def merge_request def merge_request
...@@ -430,28 +438,23 @@ module Ci ...@@ -430,28 +438,23 @@ module Ci
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end end
def project_variables
project.variables.map do |variable|
{ key: variable.key, value: variable.value, public: false }
end
end
def trigger_variables
if trigger_request && trigger_request.variables
trigger_request.variables.map do |key, value|
{ key: key, value: value, public: false }
end
else
[]
end
end
def predefined_variables def predefined_variables
variables = [] variables = [
variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag? { key: 'CI', value: 'true', public: true },
variables << { key: :CI_BUILD_NAME, value: name, public: true } { key: 'GITLAB_CI', value: 'true', public: true },
variables << { key: :CI_BUILD_STAGE, value: stage, public: true } { key: 'CI_BUILD_ID', value: id.to_s, public: true },
variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request { key: 'CI_BUILD_TOKEN', value: token, public: false },
{ key: 'CI_BUILD_REF', value: sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
]
variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
variables variables
end end
......
...@@ -20,6 +20,11 @@ module Ci ...@@ -20,6 +20,11 @@ module Ci
after_touch :update_state after_touch :update_state
after_save :keep_around_commits after_save :keep_around_commits
# ref can't be HEAD or SHA, can only be branch/tag name
scope :latest_successful_for, ->(ref = default_branch) do
where(ref: ref).success.order(id: :desc).limit(1)
end
def self.truncate_sha(sha) def self.truncate_sha(sha)
sha[0...8] sha[0...8]
end end
...@@ -146,6 +151,10 @@ module Ci ...@@ -146,6 +151,10 @@ module Ci
end end
end end
def has_warnings?
builds.latest.ignored.any?
end
def config_processor def config_processor
return nil unless ci_yaml_file return nil unless ci_yaml_file
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
...@@ -198,6 +207,12 @@ module Ci ...@@ -198,6 +207,12 @@ module Ci
Note.for_commit_id(sha) Note.for_commit_id(sha)
end end
def predefined_variables
[
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
]
end
private private
def build_builds_for_stages(stages, user, status, trigger_request) def build_builds_for_stages(stages, user, status, trigger_request)
...@@ -206,8 +221,9 @@ module Ci ...@@ -206,8 +221,9 @@ module Ci
# build builds only for the first stage that has builds available. # build builds only for the first stage that has builds available.
# #
stages.any? do |stage| stages.any? do |stage|
CreateBuildsService.new(self) CreateBuildsService.new(self).
.execute(stage, user, status, trigger_request).present? execute(stage, user, status, trigger_request).
any?(&:active?)
end end
end end
...@@ -226,7 +242,7 @@ module Ci ...@@ -226,7 +242,7 @@ module Ci
def keep_around_commits def keep_around_commits
return unless project return unless project
project.repository.keep_around(self.sha) project.repository.keep_around(self.sha)
project.repository.keep_around(self.before_sha) project.repository.keep_around(self.before_sha)
end end
......
...@@ -114,6 +114,14 @@ module Ci ...@@ -114,6 +114,14 @@ module Ci
tag_list.any? tag_list.any?
end end
def predefined_variables
[
{ key: 'CI_RUNNER_ID', value: id.to_s, public: true },
{ key: 'CI_RUNNER_DESCRIPTION', value: description, public: true },
{ key: 'CI_RUNNER_TAGS', value: tag_list.to_s, public: true }
]
end
private private
def tag_constraints def tag_constraints
......
...@@ -7,5 +7,13 @@ module Ci ...@@ -7,5 +7,13 @@ module Ci
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
serialize :variables serialize :variables
def user_variables
return [] unless variables
variables.map do |key, value|
{ key: key, value: value, public: false }
end
end
end end
end end
...@@ -16,7 +16,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -16,7 +16,11 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user alias_attribute :author, :user
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } scope :latest, -> do
max_id = unscope(:select).select("max(#{quoted_table_name}.id)")
where(id: max_id.group(:name, :commit_id))
end
scope :retried, -> { where.not(id: latest) } scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) } scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
......
module NoteOnDiff module NoteOnDiff
extend ActiveSupport::Concern extend ActiveSupport::Concern
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
included do
delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
end
def diff_note? def diff_note?
true true
end end
...@@ -30,23 +24,4 @@ module NoteOnDiff ...@@ -30,23 +24,4 @@ module NoteOnDiff
def can_be_award_emoji? def can_be_award_emoji?
false false
end end
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines
prev_lines = []
highlighted_diff_lines.each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
break if for_line?(line)
prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
prev_lines
end
end end
class Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
attr_reader :first_note, :notes
delegate :created_at,
:project,
:author,
:noteable,
:for_commit?,
:for_merge_request?,
:line_code,
:diff_file,
:for_line?,
:active?,
to: :first_note
delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
def self.for_notes(notes)
notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
end
def self.for_diff_notes(notes)
notes.group_by(&:line_code).values.map { |notes| new(notes) }
end
def initialize(notes)
@first_note = notes.first
@notes = notes
end
def id
first_note.discussion_id
end
def diff_discussion?
first_note.diff_note?
end
def legacy_diff_discussion?
notes.any?(&:legacy_diff_note?)
end
def for_target?(target)
self.noteable == target && !diff_discussion?
end
def expanded?
!diff_discussion? || active?
end
def reply_attributes
data = {
noteable_type: first_note.noteable_type,
noteable_id: first_note.noteable_id,
commit_id: first_note.commit_id,
discussion_id: self.id,
}
if diff_discussion?
data[:note_type] = first_note.type
data.merge!(first_note.diff_attributes)
end
data
end
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines
prev_lines = []
highlighted_diff_lines.each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
break if for_line?(line)
prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
prev_lines
end
end
...@@ -52,10 +52,50 @@ class Issue < ActiveRecord::Base ...@@ -52,10 +52,50 @@ class Issue < ActiveRecord::Base
attributes attributes
end end
class << self
private
# Returns the project that the current scope belongs to if any, nil otherwise.
#
# Examples:
# - my_project.issues.without_due_date.owner_project => my_project
# - Issue.all.owner_project => nil
def owner_project
# No owner if we're not being called from an association
return unless all.respond_to?(:proxy_association)
owner = all.proxy_association.owner
# Check if the association is or belongs to a project
if owner.is_a?(Project)
owner
else
begin
owner.association(:project).target
rescue ActiveRecord::AssociationNotFoundError
nil
end
end
end
end
def self.visible_to_user(user) def self.visible_to_user(user)
return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return all if user.admin? return all if user.admin?
# Check if we are scoped to a specific project's issues
if owner_project
if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER)
# If the project is authorized for the user, they can see all issues in the project
return all
else
# else only non confidential and authored/assigned to them
return where('issues.confidential IS NULL OR issues.confidential IS FALSE
OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
user_id: user.id)
end
end
where(' where('
issues.confidential IS NULL issues.confidential IS NULL
OR issues.confidential IS FALSE OR issues.confidential IS FALSE
......
...@@ -82,11 +82,12 @@ class Note < ActiveRecord::Base ...@@ -82,11 +82,12 @@ class Note < ActiveRecord::Base
end end
def discussions def discussions
all.group_by(&:discussion_id).values Discussion.for_notes(all)
end end
def grouped_diff_notes def grouped_diff_discussions
diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) notes = diff_notes.fresh.select(&:active?)
Discussion.for_diff_notes(notes).map { |d| [d.line_code, d] }.to_h
end end
# Searches for notes matching the given query. # Searches for notes matching the given query.
......
...@@ -429,6 +429,17 @@ class Project < ActiveRecord::Base ...@@ -429,6 +429,17 @@ class Project < ActiveRecord::Base
repository.commit(ref) repository.commit(ref)
end end
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
pipeline = pipelines.latest_successful_for(ref).to_sql
join_sql = "INNER JOIN (#{pipeline}) pipelines" +
" ON pipelines.id = #{Ci::Build.quoted_table_name}.commit_id"
builds.joins(join_sql).latest.with_artifacts
# TODO: Whenever we dropped support for MySQL, we could change to:
# pipeline = pipelines.latest_successful_for(ref)
# builds.where(pipeline: pipeline).latest.with_artifacts
end
def merge_base_commit(first_commit_id, second_commit_id) def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id)
repository.commit(sha) if sha repository.commit(sha) if sha
...@@ -650,6 +661,22 @@ class Project < ActiveRecord::Base ...@@ -650,6 +661,22 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end end
def external_wiki
if has_external_wiki.nil?
cache_has_external_wiki # Populate
end
if has_external_wiki
@external_wiki ||= services.external_wikis.first
else
nil
end
end
def cache_has_external_wiki
update_column(:has_external_wiki, services.external_wikis.any?)
end
def build_missing_services def build_missing_services
services_templates = Service.where(template: true) services_templates = Service.where(template: true)
...@@ -1164,4 +1191,74 @@ class Project < ActiveRecord::Base ...@@ -1164,4 +1191,74 @@ class Project < ActiveRecord::Base
def ensure_dir_exist def ensure_dir_exist
gitlab_shell.add_namespace(repository_storage_path, namespace.path) gitlab_shell.add_namespace(repository_storage_path, namespace.path)
end end
def predefined_variables
[
{ key: 'CI_PROJECT_ID', value: id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: path, public: true },
{ key: 'CI_PROJECT_PATH', value: path_with_namespace, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: namespace.path, public: true },
{ key: 'CI_PROJECT_URL', value: web_url, public: true }
]
end
def container_registry_variables
return [] unless Gitlab.config.registry.enabled
variables = [
{ key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port, public: true }
]
if container_registry_enabled?
variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true }
end
variables
end
def secret_variables
variables.map do |variable|
{ key: variable.key, value: variable.value, public: false }
end
end
# Checks if `user` is authorized for this project, with at least the
# `min_access_level` (if given).
#
# If you change the logic of this method, please also update `User#authorized_projects`
def authorized_for_user?(user, min_access_level = nil)
return false unless user
return true if personal? && namespace_id == user.namespace_id
authorized_for_user_by_group?(user, min_access_level) ||
authorized_for_user_by_members?(user, min_access_level) ||
authorized_for_user_by_shared_projects?(user, min_access_level)
end
private
def authorized_for_user_by_group?(user, min_access_level)
member = user.group_members.find_by(source_id: group)
member && (!min_access_level || member.access_level >= min_access_level)
end
def authorized_for_user_by_members?(user, min_access_level)
member = members.find_by(user_id: user)
member && (!min_access_level || member.access_level >= min_access_level)
end
def authorized_for_user_by_shared_projects?(user, min_access_level)
shared_projects = user.group_members.joins(group: :shared_projects).
where(project_group_links: { project_id: self })
if min_access_level
members_scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
shared_projects = shared_projects.where(members: members_scope)
end
shared_projects.any?
end
end end
...@@ -4,6 +4,9 @@ class SlackService < Service ...@@ -4,6 +4,9 @@ class SlackService < Service
validates :webhook, presence: true, url: true, if: :activated? validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties def initialize_properties
# Custom serialized properties initialization
self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
self.notify_only_broken_builds = true self.notify_only_broken_builds = true
...@@ -29,13 +32,15 @@ class SlackService < Service ...@@ -29,13 +32,15 @@ class SlackService < Service
end end
def fields def fields
[ default_fields =
{ type: 'text', name: 'webhook', [
placeholder: 'https://hooks.slack.com/services/...' }, { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: '#channel' }, { type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
] ]
default_fields + build_event_channels
end end
def supported_events def supported_events
...@@ -74,7 +79,10 @@ class SlackService < Service ...@@ -74,7 +79,10 @@ class SlackService < Service
end end
opt = {} opt = {}
opt[:channel] = channel if channel
event_channel = get_channel_field(object_kind) || channel
opt[:channel] = event_channel if event_channel
opt[:username] = username if username opt[:username] = username if username
if message if message
...@@ -83,8 +91,35 @@ class SlackService < Service ...@@ -83,8 +91,35 @@ class SlackService < Service
end end
end end
def event_channel_names
supported_events.map { |event| event_channel_name(event) }
end
def event_field(event)
fields.find { |field| field[:name] == event_channel_name(event) }
end
def global_fields
fields.reject { |field| field[:name].end_with?('channel') }
end
private private
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
end
def build_event_channels
supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" }
end
end
def event_channel_name(event)
"#{event}_channel"
end
def project_name def project_name
project.name_with_namespace.gsub(/\s/, '') project.name_with_namespace.gsub(/\s/, '')
end end
......
...@@ -11,16 +11,6 @@ class Repository ...@@ -11,16 +11,6 @@ class Repository
attr_accessor :path_with_namespace, :project attr_accessor :path_with_namespace, :project
def self.clean_old_archives
Gitlab::Metrics.measure(:clean_old_archives) do
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
return unless File.directory?(repository_downloads_path)
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
end
def initialize(path_with_namespace, project) def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace @path_with_namespace = path_with_namespace
@project = project @project = project
...@@ -80,7 +70,12 @@ class Repository ...@@ -80,7 +70,12 @@ class Repository
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
commit = Gitlab::Git::Commit.find(raw_repository, ref) commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
commit = ::Commit.new(commit, @project) if commit commit = ::Commit.new(commit, @project) if commit
commit commit
rescue Rugged::OdbError rescue Rugged::OdbError
...@@ -257,10 +252,10 @@ class Repository ...@@ -257,10 +252,10 @@ class Repository
# Rugged seems to throw a `ReferenceError` when given branch_names rather # Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes # than SHA-1 hashes
number_commits_behind = raw_repository. number_commits_behind = raw_repository.
count_commits_between(branch.target, root_ref_hash) count_commits_between(branch.target.sha, root_ref_hash)
number_commits_ahead = raw_repository. number_commits_ahead = raw_repository.
count_commits_between(root_ref_hash, branch.target) count_commits_between(root_ref_hash, branch.target.sha)
{ behind: number_commits_behind, ahead: number_commits_ahead } { behind: number_commits_behind, ahead: number_commits_ahead }
end end
...@@ -392,6 +387,11 @@ class Repository ...@@ -392,6 +387,11 @@ class Repository
expire_cache if exists? expire_cache if exists?
# expire cache that don't depend on repository data (when expiring)
expire_tags_cache
expire_tag_count_cache
expire_branches_cache
expire_branch_count_cache
expire_root_ref_cache expire_root_ref_cache
expire_emptiness_caches expire_emptiness_caches
expire_exists_cache expire_exists_cache
...@@ -679,9 +679,7 @@ class Repository ...@@ -679,9 +679,7 @@ class Repository
end end
def local_branches def local_branches
@local_branches ||= rugged.branches.each(:local).map do |branch| @local_branches ||= raw_repository.local_branches
Gitlab::Git::Branch.new(branch.name, branch.target)
end
end end
alias_method :branches, :local_branches alias_method :branches, :local_branches
...@@ -822,7 +820,7 @@ class Repository ...@@ -822,7 +820,7 @@ class Repository
end end
def revert(user, commit, base_branch, revert_tree_id = nil) def revert(user, commit, base_branch, revert_tree_id = nil)
source_sha = find_branch(base_branch).target source_sha = find_branch(base_branch).target.sha
revert_tree_id ||= check_revert_content(commit, base_branch) revert_tree_id ||= check_revert_content(commit, base_branch)
return false unless revert_tree_id return false unless revert_tree_id
...@@ -839,7 +837,7 @@ class Repository ...@@ -839,7 +837,7 @@ class Repository
end end
def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
source_sha = find_branch(base_branch).target source_sha = find_branch(base_branch).target.sha
cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
return false unless cherry_pick_tree_id return false unless cherry_pick_tree_id
...@@ -860,7 +858,7 @@ class Repository ...@@ -860,7 +858,7 @@ class Repository
end end
def check_revert_content(commit, base_branch) def check_revert_content(commit, base_branch)
source_sha = find_branch(base_branch).target source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha] args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit? args << { mainline: 1 } if commit.merge_commit?
...@@ -874,7 +872,7 @@ class Repository ...@@ -874,7 +872,7 @@ class Repository
end end
def check_cherry_pick_content(commit, base_branch) def check_cherry_pick_content(commit, base_branch)
source_sha = find_branch(base_branch).target source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha] args = [commit.id, source_sha]
args << 1 if commit.merge_commit? args << 1 if commit.merge_commit?
...@@ -1039,7 +1037,7 @@ class Repository ...@@ -1039,7 +1037,7 @@ class Repository
end end
def tags_sorted_by_committed_date def tags_sorted_by_committed_date
tags.sort_by { |tag| commit(tag.target).committed_date } tags.sort_by { |tag| tag.target.committed_date }
end end
def keep_around_ref_name(sha) def keep_around_ref_name(sha)
......
...@@ -17,6 +17,7 @@ class Service < ActiveRecord::Base ...@@ -17,6 +17,7 @@ class Service < ActiveRecord::Base
after_commit :reset_updated_properties after_commit :reset_updated_properties
after_commit :cache_project_has_external_issue_tracker after_commit :cache_project_has_external_issue_tracker
after_commit :cache_project_has_external_wiki
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
...@@ -25,6 +26,7 @@ class Service < ActiveRecord::Base ...@@ -25,6 +26,7 @@ class Service < ActiveRecord::Base
scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) } scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) } scope :without_defaults, -> { where(default: false) }
...@@ -80,6 +82,18 @@ class Service < ActiveRecord::Base ...@@ -80,6 +82,18 @@ class Service < ActiveRecord::Base
Gitlab::PushDataBuilder.build_sample(project, user) Gitlab::PushDataBuilder.build_sample(project, user)
end end
def event_channel_names
[]
end
def event_field(event)
nil
end
def global_fields
fields
end
def supported_events def supported_events
%w(push tag_push issue merge_request wiki_page) %w(push tag_push issue merge_request wiki_page)
end end
...@@ -212,4 +226,10 @@ class Service < ActiveRecord::Base ...@@ -212,4 +226,10 @@ class Service < ActiveRecord::Base
project.cache_has_external_issue_tracker project.cache_has_external_issue_tracker
end end
end end
def cache_project_has_external_wiki
if project && !project.destroyed?
project.cache_has_external_wiki
end
end
end end
...@@ -111,7 +111,7 @@ class User < ActiveRecord::Base ...@@ -111,7 +111,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create before_validation :signup_domain_valid?, on: :create
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
...@@ -412,6 +412,8 @@ class User < ActiveRecord::Base ...@@ -412,6 +412,8 @@ class User < ActiveRecord::Base
end end
# Returns projects user is authorized to access. # Returns projects user is authorized to access.
#
# If you change the logic of this method, please also update `Project#authorized_for_user`
def authorized_projects(min_access_level = nil) def authorized_projects(min_access_level = nil)
Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})") Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
end end
...@@ -760,29 +762,6 @@ class User < ActiveRecord::Base ...@@ -760,29 +762,6 @@ class User < ActiveRecord::Base
Project.where(id: events) Project.where(id: events)
end end
def restricted_signup_domains
email_domains = current_application_settings.restricted_signup_domains
unless email_domains.blank?
match_found = email_domains.any? do |domain|
escaped = Regexp.escape(domain).gsub('\*', '.*?')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
email_domain = Mail::Address.new(self.email).domain
email_domain =~ regexp
end
unless match_found
self.errors.add :email,
'is not whitelisted. ' +
'Email domains valid for registration are: ' +
email_domains.join(', ')
return false
end
end
true
end
def can_be_removed? def can_be_removed?
!solo_owned_groups.present? !solo_owned_groups.present?
end end
...@@ -881,4 +860,40 @@ class User < ActiveRecord::Base ...@@ -881,4 +860,40 @@ class User < ActiveRecord::Base
self.can_create_group = false self.can_create_group = false
self.projects_limit = 0 self.projects_limit = 0
end end
def signup_domain_valid?
valid = true
error = nil
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
if domain_matches?(blocked_domains, self.email)
error = 'is not from an allowed domain.'
valid = false
end
end
allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank?
if domain_matches?(allowed_domains, self.email)
valid = true
else
error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}"
valid = false
end
end
self.errors.add(:email, error) unless valid
valid
end
def domain_matches?(email_domains, email)
signup_domain = Mail::Address.new(email).domain
email_domains.any? do |domain|
escaped = Regexp.escape(domain).gsub('\*', '.*?')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
signup_domain =~ regexp
end
end
end end
...@@ -40,6 +40,6 @@ class DeleteBranchService < BaseService ...@@ -40,6 +40,6 @@ class DeleteBranchService < BaseService
def build_push_data(branch) def build_push_data(branch)
Gitlab::PushDataBuilder Gitlab::PushDataBuilder
.build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
end end
end end
...@@ -34,6 +34,6 @@ class DeleteTagService < BaseService ...@@ -34,6 +34,6 @@ class DeleteTagService < BaseService
def build_push_data(tag) def build_push_data(tag)
Gitlab::PushDataBuilder Gitlab::PushDataBuilder
.build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
end end
end end
...@@ -26,8 +26,8 @@ class GitTagPushService < BaseService ...@@ -26,8 +26,8 @@ class GitTagPushService < BaseService
unless Gitlab::Git.blank_ref?(params[:newrev]) unless Gitlab::Git.blank_ref?(params[:newrev])
tag_name = Gitlab::Git.ref_name(params[:ref]) tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name) tag = project.repository.find_tag(tag_name)
if tag && tag.target == params[:newrev] if tag && tag.object_sha == params[:newrev]
commit = project.commit(tag.target) commit = project.commit(tag.target)
commits = [commit].compact commits = [commit].compact
message = tag.message message = tag.message
......
class RepositoryArchiveCleanUpService
LAST_MODIFIED_TIME_IN_MINUTES = 120
def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES)
@mmin = mmin
@path = Gitlab.config.gitlab.repository_downloads_path
end
def execute
Gitlab::Metrics.measure(:repository_archive_clean_up) do
return unless File.directory?(path)
clean_up_old_archives
clean_up_empty_directories
end
end
private
attr_reader :mmin, :path
def clean_up_old_archives
run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete))
end
def clean_up_empty_directories
run(%W(find #{path} -not -path #{path} -type d -empty -name \*.git -maxdepth 1 -delete))
end
def run(cmd)
Gitlab::Popen.popen(cmd)
end
end
...@@ -33,16 +33,15 @@ class FileUploader < CarrierWave::Uploader::Base ...@@ -33,16 +33,15 @@ class FileUploader < CarrierWave::Uploader::Base
end end
def to_h def to_h
filename = image? ? self.file.basename : self.file.filename filename = image_or_video? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]") escaped_filename = filename.gsub("]", "\\]")
markdown = "[#{escaped_filename}](#{self.secure_url})" markdown = "[#{escaped_filename}](#{self.secure_url})"
markdown.prepend("!") if image? markdown.prepend("!") if image_or_video?
{ {
alt: filename, alt: filename,
url: self.secure_url, url: self.secure_url,
is_image: image?,
markdown: markdown markdown: markdown
} }
end end
......
# Extra methods for uploader # Extra methods for uploader
module UploaderHelper module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
VIDEO_EXT = %w[mp4 m4v mov webm ogv]
def image? def image?
img_ext = %w(png jpg jpeg gif bmp tiff) extension_match?(IMAGE_EXT)
if file.respond_to?(:extension) end
img_ext.include?(file.extension.downcase)
else def video?
# Not all CarrierWave storages respond to :extension extension_match?(VIDEO_EXT)
ext = file.path.split('.').last.downcase end
img_ext.include?(ext)
end def image_or_video?
rescue image? || video?
false end
def extension_match?(extensions)
return false unless file
extension =
if file.respond_to?(:extension)
file.extension
else
# Not all CarrierWave storages respond to :extension
File.extname(file.path).delete('.')
end
extensions.include?(extension.downcase)
end end
def file_storage? def file_storage?
......
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
Newly registered users will by default be external Newly registered users will by default be external
%fieldset %fieldset
%legend Sign-in Restrictions %legend Sign-up Restrictions
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
...@@ -122,6 +122,49 @@ ...@@ -122,6 +122,49 @@
= f.label :send_user_confirmation_email do = f.label :send_user_confirmation_email do
= f.check_box :send_user_confirmation_email = f.check_box :send_user_confirmation_email
Send confirmation email on sign-up Send confirmation email on sign-up
.form-group
= f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
.help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :domain_blacklist_enabled do
= f.check_box :domain_blacklist_enabled
Enable domain blacklist for sign ups
.form-group
.col-sm-offset-2.col-sm-10
.radio
= label_tag :blacklist_type_file do
= radio_button_tag :blacklist_type, :file
.option-title
Upload blacklist file
.radio
= label_tag :blacklist_type_raw do
= radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
.option-title
Enter blacklist manually
.form-group.blacklist-file
= f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
.col-sm-10
= f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
.help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
.form-group.blacklist-raw
= f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
.help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :after_sign_up_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
.help-block Markdown enabled
%fieldset
%legend Sign-in Restrictions
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
...@@ -147,11 +190,6 @@ ...@@ -147,11 +190,6 @@
.col-sm-10 .col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' = 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 .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
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
.help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group .form-group
= f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2' = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
...@@ -167,11 +205,6 @@ ...@@ -167,11 +205,6 @@
.col-sm-10 .col-sm-10
= f.text_area :sign_in_text, class: 'form-control', rows: 4 = f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled .help-block Markdown enabled
.form-group
= f.label :after_sign_up_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group .form-group
= f.label :help_page_text, class: 'control-label col-sm-2' = f.label :help_page_text, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
...@@ -352,4 +385,4 @@ ...@@ -352,4 +385,4 @@
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
\ No newline at end of file
...@@ -9,6 +9,10 @@ ...@@ -9,6 +9,10 @@
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
- if @group.new_record? - if @group.new_record?
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
......
%tr.notes_holder
%td.notes_line{ colspan: 2 }
%td.notes_content
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
= link_to_reply_discussion(discussion)
- note = discussion_notes.first - diff_file = discussion.diff_file
- diff_file = note.diff_file - blob = discussion.blob
- return unless diff_file
- blob = note.blob
.diff-file.file-holder .diff-file.file-holder
.file-title .file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note) = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
.diff-content.code.js-syntax-highlight .diff-content.code.js-syntax-highlight
%table %table
- note.truncated_diff_lines.each do |line| - discussion.truncated_diff_lines.each do |line|
= render "projects/diffs/line", line: line, diff_file: diff_file, plain: true = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
- if note.for_line?(line) - if discussion.for_line?(line)
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes = render "discussions/diff_discussion", discussion: discussion
- note = discussion_notes.first - expanded = discussion.expanded?
- expanded = !note.diff_note? || note.active?
%li.note.note-discussion.timeline-entry %li.note.note-discussion.timeline-entry
.timeline-entry-inner .timeline-entry-inner
.timeline-icon .timeline-icon
= link_to user_path(note.author) do = link_to user_path(discussion.author) do
= image_tag avatar_icon(note.author), class: "avatar s40" = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content .timeline-content
.discussion.js-toggle-container{ class: note.discussion_id } .discussion.js-toggle-container{ class: discussion.id }
.discussion-header .discussion-header
= link_to_member(@project, note.author, avatar: false) = link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light .inline.discussion-headline-light
= note.author.to_reference = discussion.author.to_reference
started a discussion on started a discussion on
- if note.for_commit? - if discussion.for_commit?
- commit = note.noteable - commit = discussion.noteable
- if commit - if commit
commit commit
= link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace' = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code), class: 'monospace'
- else - else
a deleted commit a deleted commit
- else - else
- if note.active? - if discussion.active?
= link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do = link_to diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) do
the diff the diff
- else - else
an outdated diff an outdated diff
= time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
.discussion-actions .discussion-actions
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
...@@ -40,7 +39,7 @@ ...@@ -40,7 +39,7 @@
Toggle discussion Toggle discussion
.discussion-body.js-toggle-content{ class: ("hide" unless expanded) } .discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
- if note.diff_note? - if discussion.diff_discussion? && discussion.diff_file
= render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes = render "discussions/diff_with_notes", discussion: discussion
- else - else
= render "projects/notes/discussions/notes", discussion_notes: discussion_notes = render "discussions/notes", discussion: discussion
- note = discussion_notes.first
.panel.panel-default .panel.panel-default
.notes{ data: { discussion_id: note.discussion_id } } .notes{ data: { discussion_id: discussion.id } }
%ul.notes.timeline %ul.notes.timeline
= render partial: "projects/notes/note", collection: discussion_notes, as: :note = render partial: "projects/notes/note", collection: discussion.notes, as: :note
= link_to_reply_discussion(note) = link_to_reply_discussion(discussion)
- note_left = notes_left.present? ? notes_left.first : nil
- note_right = notes_right.present? ? notes_right.first : nil
%tr.notes_holder %tr.notes_holder
- if note_left - if discussion_left
%td.notes_line.old %td.notes_line.old
%td.notes_content.parallel.old %td.notes_content.parallel.old
%ul.notes{ data: { discussion_id: note_left.discussion_id } } %ul.notes{ data: { discussion_id: discussion_left.id } }
= render partial: "projects/notes/note", collection: notes_left, as: :note = render partial: "projects/notes/note", collection: discussion_left.notes, as: :note
= link_to_reply_discussion(note_left, 'old') = link_to_reply_discussion(discussion_left, 'old')
- else - else
%td.notes_line.old= "" %td.notes_line.old= ""
%td.notes_content.parallel.old= "" %td.notes_content.parallel.old= ""
- if note_right - if discussion_right
%td.notes_line.new %td.notes_line.new
%td.notes_content.parallel.new %td.notes_content.parallel.new
%ul.notes{ data: { discussion_id: note_right.discussion_id } } %ul.notes{ data: { discussion_id: discussion_right.id } }
= render partial: "projects/notes/note", collection: notes_right, as: :note = render partial: "projects/notes/note", collection: discussion_right.notes, as: :note
= link_to_reply_discussion(note_right, 'new') = link_to_reply_discussion(discussion_right, 'new')
- else - else
%td.notes_line.new= "" %td.notes_line.new= ""
%td.notes_content.parallel.new= "" %td.notes_content.parallel.new= ""
...@@ -4,11 +4,7 @@ ...@@ -4,11 +4,7 @@
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
= cache [event, current_application_settings, "v2.2"] do = cache [event, current_application_settings, "v2.2"] do
- if event.author = author_avatar(event, size: 40)
= link_to user_path(event.author) do
= image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:''
- else
= image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:''
- if event.created_project? - if event.created_project?
= render "events/event/created_project", event: event = render "events/event/created_project", event: event
......
%span.event-scope
= event_preposition(event)
- if event.project
= link_to_project event.project
- else
= event.project_name
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author event
%span.event_label{class: event.action_name} %span{class: event.action_name}
- if event.target - if event.target
= event.action_name = event.action_name
%strong %strong
...@@ -10,12 +10,7 @@ ...@@ -10,12 +10,7 @@
- else - else
= event_action_name(event) = event_action_name(event)
= event_preposition(event) = render "events/event_scope", event: event
- if event.project
= link_to_project event.project
- else
= event.project_name
- if event.target.respond_to?(:title) - if event.target.respond_to?(:title)
.event-body .event-body
......
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author event
%span.event_label{class: event.action_name} %span{class: event.action_name}
= event_action_name(event) = event_action_name(event)
- if event.project - if event.project
......
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author event
%span.event_label = event.action_name
= event.action_name = event_note_title_html(event)
= event_note_title_html(event)
at
- if event.project = render "events/event_scope", event: event
= link_to_project event.project
- else
= event.project_name
.event-body .event-body
.event-note .event-note
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author event
%span.event_label.pushed #{event.action_name} #{event.ref_type} %span.pushed #{event.action_name} #{event.ref_type}
- if event.rm_ref? - if event.rm_ref?
%strong= event.ref_name %strong= event.ref_name
- else - else
%strong %strong
= link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title) = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title)
at
= link_to_project project = render "events/event_scope", event: event
- if event.push_with_commits? - if event.push_with_commits?
.event-body .event-body
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
.form-group .form-group
%hr %hr
= f.label :share_with_group_lock, class: 'control-label' do = f.label :share_with_group_lock, class: 'control-label' do
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%span %span
Merge Requests Merge Requests
%span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do = nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do = link_to dashboard_snippets_path, title: 'Snippets' do
%span %span
Snippets Snippets
......
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do = link_to explore_root_path, title: 'Projects' do
= icon('bookmark fw')
%span %span
Projects Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups' do = link_to explore_groups_path, title: 'Groups' do
= icon('group fw')
%span %span
Groups Groups
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets' do = link_to explore_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
%span %span
Snippets Snippets
= nav_link(controller: :help) do = nav_link(controller: :help) do
= link_to help_path, title: 'Help' do = link_to help_path, title: 'Help' do
= icon('question-circle fw')
%span %span
Help Help
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
%span %span
Triggers Triggers
= nav_link(controller: :badges) do = nav_link(controller: :pipelines_settings) do
= link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span %span
Badges CI/CD Pipelines
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- content_for :scripts_body_top do - content_for :scripts_body_top do
- project = @target_project || @project - project = @target_project || @project
- if @project_wiki && @page - if @project_wiki && @page
- markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id]) - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, @page.title)
- else - else
- markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
- if current_user - if current_user
......
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/cropper.js') = page_specific_javascript_tag('lib/cropper.js')
= page_specific_javascript_tag('profile/application.js') = page_specific_javascript_tag('profile/profile_bundle.js')
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
.content_list{:"data-href" => activity_project_path(@project)}
.content_list.project-activity{:"data-href" => activity_project_path(@project)}
= spinner = spinner
:javascript :javascript
......
%fieldset.builds-feature
%h5.prepend-top-0
Builds
- unless @repository.gitlab_ci_yml
.form-group
%p Builds need to be configured before you can begin using Continuous Integration.
= link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
.form-group
%p Get recent application code using the following command:
.radio
= f.label :build_allow_git_fetch_false do
= f.radio_button :build_allow_git_fetch, 'false'
%strong git clone
%br
%span.descr Slower but makes sure you have a clean dir before every build
.radio
= f.label :build_allow_git_fetch_true do
= f.radio_button :build_allow_git_fetch, 'true'
%strong git fetch
%br
%span.descr Faster
.form-group
= f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block per build in minutes
.form-group
= f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
.input-group
%span.input-group-addon /
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
%span.input-group-addon /
%p.help-block
We will use this regular expression to find test coverage output in build trace.
Leave blank if you want to disable this feature
.bs-callout.bs-callout-info
%p Below are examples of regex for existing tools:
%ul
%li
Simplecov (Ruby) -
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
%code \d+\%\s*$
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
%li
gcovr (C/C++) -
%code ^TOTAL.*\s+(\d+\%)$
%li
tap --coverage-report=text-summary (Node.js) -
%code ^Statements\s*:\s*([^%]+)
.form-group
.checkbox
= f.label :public_builds do
= f.check_box :public_builds
%strong Public builds
.help-block Allow everyone to access builds for Public and Internal projects
.form-group.append-bottom-0
= f.label :runners_token, "Runners token", class: 'label-light'
= f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
%p.help-block The secure token used to checkout project.
- page_title 'Badges'
- badges_path = namespace_project_badges_path(@project.namespace, @project)
.prepend-top-10
.panel.panel-default
.panel-heading
%b Builds badge &middot;
= @build_badge.to_html
.pull-right
= render 'shared/ref_switcher', destination: 'badges', align_right: true
.panel-body
.row
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight('.md', @build_badge.to_markdown)
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', @build_badge.to_html)
.branch-commit .branch-commit
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace"
&middot; &middot;
%span.str-truncated %span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
......
...@@ -14,16 +14,19 @@ ...@@ -14,16 +14,19 @@
%span ##{build.id} %span ##{build.id}
- if build.stuck? - if build.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') .icon-container
= icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- if defined?(retried) && retried - if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') .icon-container
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
- if defined?(ref) && ref - if defined?(ref) && ref
- if build.ref - if build.ref
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- else - else
.light none .light none
= custom_icon("icon_commit") .icon-container
= custom_icon("icon_commit")
- if defined?(commit_sha) && commit_sha - if defined?(commit_sha) && commit_sha
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
...@@ -88,4 +91,3 @@ ...@@ -88,4 +91,3 @@
- elsif build.playable? - elsif build.playable?
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= icon('play') = icon('play')
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
%p.commit-title %p.commit-title
- if commit = pipeline.commit - if commit = pipeline.commit
= commit_author_avatar(commit, size: 20) = author_avatar(commit, size: 20)
= link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
- else - else
Cant find HEAD commit for this branch Cant find HEAD commit for this branch
......
...@@ -35,8 +35,8 @@ ...@@ -35,8 +35,8 @@
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit \.gitlab-ci.yml not found in this commit
.table-holder .table-holder.pipeline-holder
%table.table.builds %table.table.builds.pipeline
%thead %thead
%tr %tr
%th Status %th Status
......
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
= cache(cache_key) do = cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
= commit_author_avatar(commit, size: 36) = author_avatar(commit, size: 36)
.commit-info-block .commit-info-block
.commit-row-title .commit-row-title
%span.item-title %span.item-title
...@@ -18,13 +19,14 @@ ...@@ -18,13 +19,14 @@
&middot; &middot;
= commit.short_id = commit.short_id
- if commit.status - if commit.status
= render_commit_status(commit, cssclass: 'visible-xs-inline') .visible-xs-inline
= render_commit_status(commit)
- if commit.description? - if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ... %a.text-expander.hidden-xs.js-toggle-button ...
.commit-actions.hidden-xs .commit-actions.hidden-xs
- if commit.status - if commit.status
= render_commit_status(commit, cssclass: 'btn btn-transparent') = render_commit_status(commit)
= clipboard_button(clipboard_text: commit.id) = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment