Commit cb4a5880 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into 8-10-stable

parents 4d0a6f17 9ca633eb
# Prefer single quotes
StringLiterals:
EnforcedStyle: single_quotes
Enabled: true
stage:
before:
- cp config/gitlab.teatro.yml config/gitlab.yml
- mkdir /apps/gitlab-satellites
- mkdir /apps/repositories
database:
- RAILS_ENV=development force=yes bundle exec rake db:create gitlab:setup
\ No newline at end of file
Please view this file on the master branch, on stable branches it's out of date.
v 8.10.0 (unreleased)
- Expose {should,force}_remove_source_branch (Ben Boeckel)
- Fix commit builds API, return all builds for all pipelines for given commit. !4849
- Replace Haml with Hamlit to make view rendering faster. !3666
- Expire the branch cache after `git gc` runs
- Refactor repository paths handling to allow multiple git mount points
- Optimize system note visibility checking by memoizing the visible reference count !5070
- Add Application Setting to configure default Repository Path for new projects
- Delete award emoji when deleting a user
- Remove pinTo from Flash and make inline flash messages look nicer !4854 (winniehell)
- Wrap code blocks on Activies and Todos page. !4783 (winniehell)
- Align flash messages with left side of page content !4959 (winniehell)
- Display tooltip for "Copy to Clipboard" button !5164 (winniehell)
- Display last commit of deleted branch in push events !4699 (winniehell)
- Escape file extension when parsing search results !5141 (winniehell)
- Apply the trusted_proxies config to the rack request object for use with rack_attack
- Add Sidekiq queue duration to transaction metrics.
- Add a new column `artifacts_size` to table `ci_builds` !4964
- Let Workhorse serve format-patch diffs
- Added day name to contribution calendar tooltips
- Make images fit to the size of the viewport !4810
- Fix check for New Branch button on Issue page !4630 (winniehell)
- Fix MR-auto-close text added to description. !4836
......@@ -26,26 +33,36 @@ v 8.10.0 (unreleased)
- Exclude email check from the standard health check
- Updated layout for Projects, Groups, Users on Admin area !4424
- Fix changing issue state columns in milestone view
- Update health_check gem to version 2.1.0
- Add notification settings dropdown for groups
- Render inline diffs for multiple changed lines following eachother
- Wildcards for protected branches. !4665
- Allow importing from Github using Personal Access Tokens. (Eric K Idema)
- API: Todos !3188 (Robert Schilling)
- API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling)
- Add "Enabled Git access protocols" to Application Settings
- Diffs will create button/diff form on demand no on server side
- Reduce size of HTML used by diff comment forms
- Protected branches have a "Developers can Merge" setting. !4892 (original implementation by Mathias Vestergaard)
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- Only show New Snippet button to users that can create snippets.
- PipelinesFinder uses git cache data
- Throttle the update of `project.pushes_since_gc` to 1 minute.
- Allow expanding and collapsing files in diff view (!4990)
- Collapse large diffs by default (!4990)
- Check for conflicts with existing Project's wiki path when creating a new project.
- Show last push widget in upstream after push to fork
- Cache todos pending/done dashboard query counts.
- Don't instantiate a git tree on Projects show default view
- Bump Rinku to 2.0.0
- Remove unused front-end variable -> default_issues_tracker
- ObjectRenderer retrieve renderer content using Rails.cache.read_multi
- Better caching of git calls on ProjectsController#show.
- Avoid to retrieve MR closes_issues as much as possible.
- Add API endpoint for a group issues !4520 (mahcsig)
- Add Bugzilla integration !4930 (iamtjg)
- Instrument Rinku usage
- Be explicit to define merge request discussion variables
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
......@@ -60,9 +77,27 @@ v 8.10.0 (unreleased)
- Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests
- Add date when user joined the team on the member page
- Fix 404 redirect after validation fails importing a GitLab project
- Fix 404 redirect after validation fails importing a GitLab project
- Added setting to set new users by default as external !4545 (Dravere)
- Add min value for project limit field on user's form !3622 (jastkand)
- Reset project pushes_since_gc when we enqueue the git gc call
- Add reminder to not paste private SSH keys !4399 (Ingo Blechschmidt)
- Remove duplicate `description` field in `MergeRequest` entities (Ben Boeckel)
- Style of import project buttons were fixed in the new project page. !5183 (rdemirbay)
- Fix GitHub client requests when rate limit is disabled
- Optimistic locking for Issues and Merge Requests (Title and description overriding prevention)
- Redesign Builds and Pipelines pages
- Change status color and icon for running builds
v 8.9.6
- Fix importing of events under notes for GitLab projects. !5154
- Fix log statements in import/export. !5129
- Fix commit avatar alignment in compare view. !5128
- Fix broken migration in MySQL. !5005
- Overwrite Host and X-Forwarded-Host headers in NGINX !5213
v 8.9.6 (unreleased)
- Fix importing of events under notes for GitLab projects
v 8.9.5
- Add more debug info to import/export and memory killer. !5108
......@@ -2611,13 +2646,13 @@ v 6.5.0
- Files API supports base64 encoded content (sponsored by O'Reilly Media)
- Added support for Go's repository retrieval (Bruno Albuquerque)
v6.4.3
v 6.4.3
- Don't use unicorn worker killer if PhusionPassenger is defined
v6.4.2
v 6.4.2
- Fixed wrong behaviour of script/upgrade.rb
v6.4.1
v 6.4.1
- Fixed bug with repository rename
- Fixed bug with project transfer
......
......@@ -344,7 +344,7 @@ gem 'oauth2', '~> 1.2.0'
gem 'paranoia', '~> 2.0'
# Health check
gem 'health_check', '~> 1.5.1'
gem 'health_check', '~> 2.1.0'
# System information
gem 'vmstat', '~> 2.1.0'
......
......@@ -322,8 +322,8 @@ GEM
thor
tilt
hashie (3.4.3)
health_check (1.5.1)
rails (>= 2.3.0)
health_check (2.1.0)
rails (>= 4.0)
hipchat (1.5.2)
httparty
mimemagic
......@@ -870,7 +870,7 @@ DEPENDENCIES
grape (~> 0.13.0)
grape-entity (~> 0.4.2)
hamlit (~> 2.5)
health_check (~> 1.5.1)
health_check (~> 2.1.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
......
......@@ -47,7 +47,6 @@
#= require date.format
#= require_directory ./behaviors
#= require_directory ./blob
#= require_directory ./ci
#= require_directory ./commit
#= require_directory ./extensions
#= require_directory ./lib/utils
......
class @CiBuild
class @Build
@interval: null
@state: null
constructor: (@page_url, @build_url, @build_status, @state) ->
clearInterval(CiBuild.interval)
clearInterval(Build.interval)
# Init breakpoint checker
@bp = Breakpoints.get()
......@@ -40,7 +40,7 @@ class @CiBuild
# Check for new build output if user still watching build page
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
Build.interval = setInterval =>
if window.location.href.split("#").first() is @page_url
@getBuildTrace()
, 4000
......
#= require pager
#= require jquery_nested_form
#= require_tree .
$(document).on 'click', '.assign-all-runner', ->
$(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..')
window.unbindEvents = ->
$(document).unbind('scroll')
$(document).off('scroll')
document.addEventListener("page:fetch", unbindEvents)
$(document).on 'click', '.badge-codes-toggle', ->
$('.badge-codes-block').toggleClass("hide")
return false
class @Diff
UNFOLD_COUNT = 20
constructor: ->
$('.files .diff-file').singleFileDiff()
@filesCommentButton = $('.files .diff-file').filesCommentButton()
$(document).off('click', '.js-unfold')
$(document).on('click', '.js-unfold', (event) =>
target = $(event.target)
......@@ -36,7 +39,7 @@ class @Diff
# see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
indent: 1
$.get(link, params, (response) =>
$.get(link, params, (response) ->
target.parent().replaceWith(response)
)
)
......
class @FilesCommentButton
COMMENT_BUTTON_CLASS = '.add-diff-note'
COMMENT_BUTTON_TEMPLATE = _.template '<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'
LINE_HOLDER_CLASS = '.line_holder'
LINE_NUMBER_CLASS = 'diff-line-num'
LINE_CONTENT_CLASS = 'line_content'
UNFOLDABLE_LINE_CLASS = 'js-unfold'
EMPTY_CELL_CLASS = 'empty-cell'
OLD_LINE_CLASS = 'old_line'
NEW_CLASS = 'new'
LINE_COLUMN_CLASSES = ".#{LINE_NUMBER_CLASS}, .line_content"
TEXT_FILE_SELECTOR = '.text-file'
DEBOUNCE_TIMEOUT_DURATION = 100
constructor: (@filesContainerElement) ->
@VIEW_TYPE = $('input#view[type=hidden]').val()
debounce = _.debounce @render, DEBOUNCE_TIMEOUT_DURATION
$(document)
.on 'mouseover', LINE_COLUMN_CLASSES, debounce
.on 'mouseleave', LINE_COLUMN_CLASSES, @destroy
render: (e) =>
$currentTarget = $(e.currentTarget)
buttonParentElement = @getButtonParent $currentTarget
return unless @shouldRender e, buttonParentElement
textFileElement = @getTextFileElement $currentTarget
lineContentElement = @getLineContent $currentTarget
buttonParentElement.append @buildButton
noteableType: textFileElement.attr 'data-noteable-type'
noteableID: textFileElement.attr 'data-noteable-id'
commitID: textFileElement.attr 'data-commit-id'
noteType: lineContentElement.attr 'data-note-type'
position: lineContentElement.attr 'data-position'
lineType: lineContentElement.attr 'data-line-type'
discussionID: lineContentElement.attr 'data-discussion-id'
lineCode: lineContentElement.attr 'data-line-code'
return
destroy: (e) =>
return if @isMovingToSameType e
$(COMMENT_BUTTON_CLASS, @getButtonParent $(e.currentTarget)).remove()
return
buildButton: (buttonAttributes) ->
initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE
COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr 1
$(initializedButtonTemplate).attr
'data-noteable-type': buttonAttributes.noteableType
'data-noteable-id': buttonAttributes.noteableID
'data-commit-id': buttonAttributes.commitID
'data-note-type': buttonAttributes.noteType
'data-line-code': buttonAttributes.lineCode
'data-position': buttonAttributes.position
'data-discussion-id': buttonAttributes.discussionID
'data-line-type': buttonAttributes.lineType
getTextFileElement: (hoveredElement) ->
$(hoveredElement.closest TEXT_FILE_SELECTOR)
getLineContent: (hoveredElement) ->
return hoveredElement if hoveredElement.hasClass LINE_CONTENT_CLASS
$(".#{LINE_CONTENT_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent())
getButtonParent: (hoveredElement) ->
if @VIEW_TYPE is 'inline'
return hoveredElement if hoveredElement.hasClass OLD_LINE_CLASS
$(".#{OLD_LINE_CLASS}", hoveredElement.parent())
else
return hoveredElement if hoveredElement.hasClass LINE_NUMBER_CLASS
$(".#{LINE_NUMBER_CLASS + @diffTypeClass hoveredElement}", hoveredElement.parent())
diffTypeClass: (hoveredElement) ->
if hoveredElement.hasClass(NEW_CLASS) then '.new' else '.old'
isMovingToSameType: (e) ->
newButtonParent = @getButtonParent $(e.toElement)
return false unless newButtonParent
newButtonParent.is @getButtonParent $(e.currentTarget)
shouldRender: (e, buttonParentElement) ->
(not buttonParentElement.hasClass(EMPTY_CELL_CLASS) and \
not buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) and \
$(COMMENT_BUTTON_CLASS, buttonParentElement).length is 0)
$.fn.filesCommentButton = ->
return unless this and @parent().data('can-create-note')?
@each ->
unless $.data this, 'filesCommentButton'
$.data this, 'filesCommentButton', new FilesCommentButton $(this)
class @Flash
constructor: (message, type = 'alert')->
@flash = $(".flash-container")
@flash.html("")
hideFlash = -> $(@).fadeOut()
innerDiv = $('<div/>',
constructor: (message, type = 'alert', parent = null)->
if parent
@flashContainer = parent.find('.flash-container')
else
@flashContainer = $('.flash-container-page')
@flashContainer.html('')
flash = $('<div/>',
class: "flash-#{type}"
)
innerDiv.appendTo(".flash-container")
flash.on 'click', hideFlash
textDiv = $("<div/>",
class: "flash-text",
textDiv = $('<div/>',
class: 'flash-text',
text: message
)
textDiv.appendTo(innerDiv)
textDiv.appendTo(flash)
if @flash.parent().hasClass('content-wrapper')
if @flashContainer.parent().hasClass('content-wrapper')
textDiv.addClass('container-fluid container-limited')
@flash.click -> $(@).fadeOut()
@flash.show()
flash.appendTo(@flashContainer)
@flashContainer.show()
pinTo: (selector) ->
@flash.detach().appendTo(selector)
......@@ -2,10 +2,14 @@
w.gl ?= {}
w.gl.utils ?= {}
w.gl.utils.days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
w.gl.utils.formatDate = (datetime) ->
dateFormat(datetime, 'mmm d, yyyy h:MMtt Z')
w.gl.utils.getDayName = (date) ->
this.days[date.getDay()]
w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) ->
$timeagoEls.each( ->
$el = $(@)
......
......@@ -153,17 +153,18 @@ class @MergeRequestTabs
loadDiff: (source) ->
return if @diffsLoaded
@_get
url: "#{source}.json" + @_location.search
success: (data) =>
$('#diffs').html data.html
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
$('#diffs .js-syntax-highlight').syntaxHighlight()
$('#diffs .diff-file').singleFileDiff()
@expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
@scrollToElement("#diffs")
@highlighSelectedLine()
@filesCommentButton = $('.files .diff-file').filesCommentButton()
$(document)
.off 'click', '.diff-line-num a'
......
......@@ -194,8 +194,7 @@ class @Notes
renderNote: (note) ->
unless note.valid
if note.award
flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content')
new Flash('You have already awarded this emoji!', 'alert')
return
if note.award
......@@ -325,6 +324,8 @@ class @Notes
form.find("#note_position").remove()
form.find("#note_type").remove()
@parentTimeline = form.parents('.timeline')
###
General note form setup.
......@@ -357,8 +358,7 @@ class @Notes
@renderNote(note)
addNoteError: (xhr, note, status) =>
flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
flash.pinTo('.md-area')
new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', @parentTimeline)
###
Called in response to the new note form being submitted
......
$ ->
$(".protected-branches-list :checkbox").change (e) ->
name = $(this).attr("name")
if name == "developers_can_push"
if name == "developers_can_push" || name == "developers_can_merge"
id = $(this).val()
checked = $(this).is(":checked")
can_push = $(this).is(":checked")
url = $(this).data("url")
$.ajax
type: "PUT"
type: "PATCH"
url: url
dataType: "json"
data:
id: id
protected_branch:
developers_can_push: checked
"#{name}": can_push
success: ->
row = $(e.target)
......
class @SingleFileDiff
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'
constructor: (@file) ->
@content = $('.diff-content', @file)
@diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path'
@isOpen = !@diffForPath
if @diffForPath
@collapsedContent = @content
@loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide()
@content = null
@collapsedContent.after(@loadingContent)
else
@collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide()
@content.after(@collapsedContent)
@collapsedContent.on 'click', @toggleDiff
$('.file-title > a', @file).on 'click', @toggleDiff
toggleDiff: (e) =>
@isOpen = !@isOpen
if not @isOpen and not @hasError
@content.hide()
@collapsedContent.show()
else if @content
@collapsedContent.hide()
@content.show()
else
@getContentHTML()
getContentHTML: ->
@collapsedContent.hide()
@loadingContent.show()
$.get @diffForPath, (data) =>
@loadingContent.hide()
if data.html
@content = $(data.html)
@content.syntaxHighlight()
else
@hasError = true
@content = $(ERROR_HTML)
@collapsedContent.after(@content)
return
$.fn.singleFileDiff = ->
return @each ->
if not $.data this, 'singleFileDiff'
$.data this, 'singleFileDiff', new SingleFileDiff this
......@@ -87,14 +87,15 @@ class @Calendar
.attr 'width', @daySize
.attr 'height', @daySize
.attr 'title', (stamp) =>
date = new Date(stamp.date)
contribText = 'No contributions'
if stamp.count > 0
contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}"
date = dateFormat(stamp.date, 'mmm d, yyyy')
dateText = dateFormat(date, 'mmm d, yyyy')
"#{contribText}<br />#{date}"
"#{contribText}<br />#{gl.utils.getDayName(date)} #{dateText}"
.attr 'class', 'user-contrib-cell js-tooltip'
.attr 'fill', (stamp) =>
if stamp.count isnt 0
......
......@@ -20,6 +20,7 @@
}
&.s16 { width: 16px; height: 16px; margin-right: 6px; }
&.s20 { width: 20px; height: 20px; margin-right: 7px; }
&.s24 { width: 24px; height: 24px; margin-right: 8px; }
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
......
......@@ -16,6 +16,9 @@
font-weight: normal;
font-size: 16px;
line-height: 36px;
&.diff-collapsed {
cursor: pointer;
}
}
.row-content-block {
......
.flash-container {
cursor: pointer;
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
width: 100%;
z-index: 100;
.flash-notice {
......@@ -18,9 +18,27 @@
}
.flash-notice, .flash-alert {
.container-fluid.flash-text {
border-radius: $border-radius-default;
.container-fluid.container-limited.flash-text {
background: transparent;
}
}
&.flash-container-page {
margin-bottom: 0;
.flash-notice, .flash-alert {
border-radius: 0;
}
}
}
@media (max-width: $screen-md-min) {
ul.notes {
.flash-container.timeline-content {
margin-left: 0;
}
}
}
......@@ -11,7 +11,6 @@
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
background: $color;
&:hover {
color: $white-light;
......
......@@ -3,6 +3,12 @@
padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
.sidebar-wrapper {
@include box-shadow(none);
}
}
.sidebar-wrapper {
position: fixed;
top: 0;
......@@ -11,6 +17,7 @@
height: 100%;
overflow: hidden;
transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent);
}
}
......@@ -69,7 +76,7 @@
}
a {
padding: 7px 15px 7px 12px;
padding: 7px 16px;
font-size: $gl-font-size;
line-height: 24px;
display: block;
......@@ -101,7 +108,7 @@
}
}
.toggle-nav-collapse {
.sidebar-action-buttons {
width: $sidebar_width;
position: absolute;
top: 0;
......@@ -110,28 +117,14 @@
padding: 5px 0;
font-size: 18px;
line-height: 30px;
}
.nav-header-btn {
padding: 10px 5px;
color: inherit;
transition-duration: .3s;
&:hover,
&:focus {
color: $white-light;
text-decoration: none;
.toggle-nav-collapse {
left: 0;
}
}
.pin-nav-btn {
.pin-nav-btn {
right: 0;
display: none;
position: absolute;
left: 0;
bottom: 0;
height: 50px;
width: $sidebar_width;
line-height: 30px;
@media (min-width: $sidebar-breakpoint) {
display: block;
......@@ -146,6 +139,21 @@
transform: rotate(90deg);
}
}
}
}
.nav-header-btn {
padding: 10px 16px;
color: inherit;
transition-duration: .3s;
position: absolute;
top: 0;
&:hover,
&:focus {
color: $white-light;
text-decoration: none;
}
}
.page-sidebar-collapsed {
......
......@@ -17,6 +17,7 @@ $focus-border-color: #3aabf0;
$table-border-color: #f0f0f0;
$background-color: #fafafa;
$dark-background-color: #f7f7f7;
$table-text-gray: #8f8f8f;
/*
* Text
......
......@@ -83,14 +83,6 @@
}
}
table.builds {
.build-link {
a {
color: $gl-dark-link-color;
}
}
}
.build-trace {
background: $ci-output-bg;
color: $ci-text-color;
......
......@@ -61,7 +61,7 @@
font-size: 0;
}
.btn-transparent {
.btn-clipboard, .btn-transparent {
padding-left: 0;
padding-right: 0;
}
......
......@@ -63,8 +63,8 @@ form.edit-issue {
.merge-request,
.issue {
&.today {
background: #efe;
border-color: #cec;
background: #f8feef;
border-color: #e1e8d5;
}
&.closed {
......
......@@ -162,9 +162,15 @@
}
.filtered-labels {
font-size: 0;
padding: 12px 16px;
.label-row {
margin-top: 4px;
margin-bottom: 4px;
&:not(:last-child) {
margin-right: 5px;
margin-right: 8px;
}
}
......
......@@ -73,11 +73,14 @@
color: #888;
}
&.ci-pending,
&.ci-running {
&.ci-pending {
color: $gl-warning;
}
&.ci-running {
color: $blue-normal;
}
&.ci-failed,
&.ci-error {
color: $gl-danger;
......
.pipelines {
.stage {
max-width: 100px;
max-width: 80px;
width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.duration, .finished_at {
margin: 4px 0;
}
.commit-title {
margin: 0;
}
......@@ -22,3 +19,136 @@
margin: 4px;
}
}
.content-list {
&.pipelines,
&.builds-content-list {
width: 100%;
overflow: auto;
}
}
.table.builds {
min-width: 1100px;
tr {
th {
padding: 16px;
border: none;
}
}
tbody {
border-top-width: 1px;
}
.branch-commit {
.branch-name {
margin-left: 8px;
font-weight: bold;
max-width: 180px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
vertical-align: top;
text-overflow: ellipsis;
}
svg {
margin: 0 6px;
height: 14px;
width: auto;
vertical-align: middle;
}
.commit-id {
color: $gl-link-color;
margin-right: 8px;
}
.commit-title {
margin-top: 4px;
max-width: 320px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.avatar {
margin-left: 0;
}
.label-container {
.label {
margin-top: 5px;
}
}
}
.duration,
.finished-at {
color: $table-text-gray;
margin: 4px 0;
.fa {
font-size: 12px;
}
svg {
height: 12px;
width: auto;
vertical-align: middle;
}
.fa,
svg {
margin-right: 5px;
}
}
.pipeline-actions {
.btn {
margin: 0;
color: $table-text-gray;
}
.dropdown-toggle,
.dropdown-menu {
color: $table-text-gray;
.fa {
color: $table-text-gray;
margin-right: 6px;
font-size: 14px;
}
}
.btn-remove {
color: $white-light;
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
}
}
.build-link {
a {
color: $gl-dark-link-color;
}
}
.btn-group.open .dropdown-toggle {
box-shadow: none;
}
}
......@@ -340,23 +340,30 @@ a.deploy-project-label {
.project-import {
.form-group {
margin-bottom: 0;
margin-bottom: 5px;
}
.import-buttons {
padding-left: 0;
display: -webkit-flex;
display: flex;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
.btn {
margin-right: 10px;
padding: 8px 12px;
margin: 0 10px 10px 0;
padding: 8px;
}
&> div {
margin-bottom: 14px;
> div {
padding-left: 0;
&:last-child {
margin-bottom: 0;
.btn {
margin-right: 0;
}
}
}
}
......
......@@ -32,11 +32,15 @@
border-color: $gl-gray;
}
&.ci-pending,
&.ci-running {
&.ci-pending {
color: $gl-warning;
border-color: $gl-warning;
}
&.ci-running {
color: $blue-normal;
border-color: $blue-normal;
}
}
.ci-status-icon-success {
......@@ -45,10 +49,12 @@
.ci-status-icon-failed {
color: $gl-danger;
}
.ci-status-icon-running,
.ci-status-icon-pending {
color: $gl-warning;
}
.ci-status-icon-running {
color: $blue-normal;
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found,
......
module DiffForPath
extend ActiveSupport::Concern
def render_diff_for_path(diffs, diff_refs, project)
diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff|
diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
end
return render_404 unless diff_file
diff_commit = commit_for_diff(diff_file)
blob = diff_file.blob(diff_commit)
@expand_all_diffs = true
locals = {
diff_file: diff_file,
diff_commit: diff_commit,
diff_refs: diff_refs,
blob: blob,
project: project
}
render json: { html: view_to_html_string('projects/diffs/_content', locals) }
end
end
class Dashboard::TodosController < Dashboard::ApplicationController
include TodosHelper
before_action :find_todos, only: [:index, :destroy_all]
def index
......@@ -13,7 +11,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { head :ok }
format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
format.json { render json: todos_counts }
end
end
......@@ -23,7 +21,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
format.json { render json: todos_counts }
end
end
......@@ -36,4 +34,11 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def find_todos
@todos ||= TodosFinder.new(current_user, params).execute
end
def todos_counts
{
count: TodosFinder.new(current_user, state: :pending).execute.count,
done_count: TodosFinder.new(current_user, state: :done).execute.count
}
end
end
......@@ -12,13 +12,12 @@ class HelpController < ApplicationController
end
def show
@category = clean_path_info(path_params[:category])
@file = path_params[:file]
@path = clean_path_info(path_params[:path])
respond_to do |format|
format.any(:markdown, :md, :html) do
# Note: We are purposefully NOT using `Rails.root.join`
path = File.join(Rails.root, 'doc', @category, "#{@file}.md")
path = File.join(Rails.root, 'doc', "#{@path}.md")
if File.exist?(path)
@markdown = File.read(path)
......@@ -33,7 +32,7 @@ class HelpController < ApplicationController
# Allow access to images in the doc folder
format.any(:png, :gif, :jpeg) do
# Note: We are purposefully NOT using `Rails.root.join`
path = File.join(Rails.root, 'doc', @category, "#{@file}.#{params[:format]}")
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
if File.exist?(path)
send_file(path, disposition: 'inline')
......@@ -57,8 +56,7 @@ class HelpController < ApplicationController
private
def path_params
params.require(:category)
params.require(:file)
params.require(:path)
params
end
......
......@@ -107,7 +107,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Only allow properly saved users to login.
if @user.persisted? && @user.valid?
log_audit_event(@user, with: oauth['provider'])
if @user.two_factor_enabled?
prompt_for_two_factor(@user)
else
sign_in_and_redirect(@user)
end
else
error_message = @user.errors.full_messages.to_sentence
......
......@@ -23,10 +23,9 @@ class Projects::ArtifactsController < Projects::ApplicationController
entry = build.artifacts_metadata_entry(params[:path])
if entry.exists?
render json: { archive: build.artifacts_file.path,
entry: Base64.encode64(entry.path) }
send_artifacts_entry(build, entry)
else
render json: {}, status: 404
render_404
end
end
......
......@@ -3,6 +3,7 @@
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
include CreatesCommit
include DiffForPath
include DiffHelper
# Authorize
......@@ -11,29 +12,14 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
before_action :define_show_vars, only: [:show, :builds]
before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
before_action :define_status_vars, only: [:show, :builds]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
def show
apply_diff_view_cookie!
@grouped_diff_notes = commit.notes.grouped_diff_notes
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten + @notes,
@project,
current_user,
)
@note = @project.build_commit_note(commit)
@noteable = @commit
@comments_target = {
noteable_type: 'Commit',
commit_id: @commit.id
}
respond_to do |format|
format.html
format.diff { render text: @commit.to_diff }
......@@ -41,6 +27,10 @@ class Projects::CommitController < Projects::ApplicationController
end
end
def diff_for_path
render_diff_for_path(@diffs, @commit.diff_refs, @project)
end
def builds
end
......@@ -114,7 +104,7 @@ class Projects::CommitController < Projects::ApplicationController
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
def define_show_vars
def define_commit_vars
return git_not_found! unless commit
opts = diff_options
......@@ -122,7 +112,28 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
end
def define_note_vars
@grouped_diff_notes = commit.notes.grouped_diff_notes
@notes = commit.notes.non_diff_notes.fresh
Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten + @notes,
@project,
current_user,
)
@note = @project.build_commit_note(commit)
@noteable = @commit
@comments_target = {
noteable_type: 'Commit',
commit_id: @commit.id
}
end
def define_status_vars
@statuses = CommitStatus.where(pipeline: pipelines)
@builds = Ci::Build.where(pipeline: pipelines)
end
......
require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
include DiffForPath
include DiffHelper
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :assign_ref_vars, only: [:index, :show]
before_action :define_ref_vars, only: [:index, :show, :diff_for_path]
before_action :define_diff_vars, only: [:show, :diff_for_path]
before_action :merge_request, only: [:index, :show]
def index
end
def show
compare = CompareService.new.
execute(@project, @head_ref, @project, @start_ref, diff_options)
end
def diff_for_path
return render_404 unless @compare
render_diff_for_path(@diffs, @diff_refs, @project)
end
def create
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
end
private
if compare
@commits = Commit.decorate(compare.commits, @project)
def define_ref_vars
@start_ref = Addressable::URI.unescape(params[:from])
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
def define_diff_vars
@compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
if @compare
@commits = Commit.decorate(@compare.commits, @project)
@start_commit = @project.commit(@start_ref)
@commit = @project.commit(@head_ref)
@base_commit = @project.merge_base_commit(@start_ref, @head_ref)
@diffs = compare.diffs(diff_options)
@diffs = @compare.diffs(diff_options)
@diff_refs = Gitlab::Diff::DiffRefs.new(
base_sha: @base_commit.try(:sha),
start_sha: @start_commit.try(:sha),
......@@ -35,18 +57,6 @@ class Projects::CompareController < Projects::ApplicationController
end
end
def create
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
end
private
def assign_ref_vars
@start_ref = Addressable::URI.unescape(params[:from])
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
def merge_request
@merge_request ||= @project.merge_requests.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
......
......@@ -119,6 +119,10 @@ class Projects::IssuesController < Projects::ApplicationController
render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
end
end
rescue ActiveRecord::StaleObjectError
@conflict = true
render :edit
end
def referenced_merge_requests
......@@ -216,7 +220,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
:milestone_id, :due_date, :state_event, :task_num, label_ids: []
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
......
class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffForPath
include DiffHelper
include IssuableActions
include ToggleAwardEmoji
......@@ -12,6 +13,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request
......@@ -53,7 +56,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def show
respond_to do |format|
format.html
format.html { define_discussion_vars }
format.json do
render json: @merge_request
......@@ -78,35 +81,38 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request.merge_request_diff
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
@comments_target = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
@use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
@grouped_diff_notes = @merge_request.notes.grouped_diff_notes
Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten,
@project,
current_user,
@path,
@project_wiki,
@ref
)
respond_to do |format|
format.html
format.html { define_discussion_vars }
format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
end
end
# With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new
# and uses that (unsaved) MR.
#
def diff_for_path
if params[:id]
merge_request
define_diff_comment_vars
else
build_merge_request
@diff_notes_disabled = true
@grouped_diff_notes = {}
end
define_commit_vars
diffs = @merge_request.diffs(diff_options)
render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project)
end
def commits
respond_to do |format|
format.html { render 'show' }
format.html do
define_discussion_vars
render 'show'
end
format.json do
# Get commits from repository
# or from cache if already merged
......@@ -121,14 +127,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def builds
respond_to do |format|
format.html { render 'show' }
format.html do
define_discussion_vars
render 'show'
end
format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
end
end
def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
......@@ -187,6 +196,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else
render "edit"
end
rescue ActiveRecord::StaleObjectError
@conflict = true
render :edit
end
def remove_wip
......@@ -352,14 +364,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.unlock_mr
@merge_request.close
end
if request.format == :html || action_name == 'show'
define_show_html_vars
end
end
# Discussion tab data is only required on html requests
def define_show_html_vars
# Discussion tab data is rendered on html responses of actions
# :show, :diff, :commits, :builds. but not when request the data through AJAX
def define_discussion_vars
# Build a note object for comment form
@note = @project.notes.new(noteable: @noteable)
......@@ -384,6 +393,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@pipelines = [@pipeline].compact
end
def define_commit_vars
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
end
def define_diff_comment_vars
@comments_target = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
@use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
@grouped_diff_notes = @merge_request.notes.grouped_diff_notes
Banzai::NoteRenderer.render(
@grouped_diff_notes.values.flatten,
@project,
current_user,
@path,
@project_wiki,
@ref
)
end
def invalid_mr
# Render special view for MR with removed source or target branch
render 'invalid'
......@@ -394,7 +427,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :task_num, :force_remove_source_branch,
label_ids: []
:lock_version, label_ids: []
)
end
......@@ -412,4 +445,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_when_build_succeeds].present? &&
@merge_request.pipeline && @merge_request.pipeline.active?
end
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end
end
......@@ -50,6 +50,6 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end
def protected_branch_params
params.require(:protected_branch).permit(:name, :developers_can_push)
params.require(:protected_branch).permit(:name, :developers_can_push, :developers_can_merge)
end
end
......@@ -5,7 +5,7 @@ class Projects::TodosController < Projects::ApplicationController
todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
count: current_user.todos_pending_count,
count: TodosFinder.new(current_user, state: :pending).execute.count,
delete_path: dashboard_todo_path(todo)
}
end
......
......@@ -8,7 +8,7 @@
# action_id: integer
# author_id: integer
# project_id; integer
# state: 'pending' or 'done'
# state: 'pending' (default) or 'done'
# type: 'Issue' or 'MergeRequest'
#
......@@ -37,7 +37,7 @@ class TodosFinder
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end
def action_id
......
......@@ -31,7 +31,7 @@ module AppearancesHelper
end
end
def navbar_icon(icon_name, size: 16)
def custom_icon(icon_name, size: 16)
render "shared/icons/#{icon_name}.svg", size: size
end
end
......@@ -12,7 +12,7 @@ module BranchesHelper
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_exists?(branch_name)
::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(branch_name)
::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch_name)
end
def project_branches
......
......@@ -15,29 +15,13 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
class: "btn btn-clipboard",
data: data,
type: :button
end
# Output a "Copy to Clipboard" button with a custom CSS class
#
# data - Data attributes passed to `content_tag`
# css_class - Class passed to the `content_tag`
#
# Examples:
#
# # Define the target element
# clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
# # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data,
type: :button
type: :button,
title: "Copy to Clipboard"
end
def http_clone_button(project, placement = 'right', append_link: true)
......
......@@ -29,8 +29,10 @@ module CiStatusHelper
'check'
when 'failed'
'close'
when 'running', 'pending'
when 'pending'
'clock-o'
when 'running'
'spinner'
else
'circle'
end
......
......@@ -8,6 +8,10 @@ module DiffHelper
[marked_old_line, marked_new_line]
end
def expand_all_diffs?
@expand_all_diffs || params[:expand_all_diffs].present?
end
def diff_view
diff_views = %w(inline parallel)
......@@ -18,16 +22,14 @@ module DiffHelper
end
end
def diff_hard_limit_enabled?
params[:force_show_diff].present?
end
def diff_options
options = { ignore_whitespace_change: hide_whitespace? }
if diff_hard_limit_enabled?
options.merge!(Commit.max_diff_options)
default_options = Commit.max_diff_options
if action_name == 'diff_for_path'
default_options[:paths] = params.values_at(:old_path, :new_path)
end
options
default_options.merge(ignore_whitespace_change: hide_whitespace?)
end
def safe_diff_files(diffs, diff_refs: nil, repository: nil)
......@@ -35,7 +37,7 @@ module DiffHelper
end
def unfold_bottom_class(bottom)
bottom ? 'js-unfold-bottom' : ''
bottom ? 'js-unfold js-unfold-bottom' : ''
end
def unfold_class(unfold)
......
......@@ -96,4 +96,8 @@ module MergeRequestsHelper
["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"]
end
end
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?)
end
end
......@@ -24,7 +24,15 @@ module NotesHelper
}.to_json
end
def link_to_new_diff_note(line_code, position, line_type = nil)
def diff_view_data
return {} unless @comments_target
@comments_target.slice(:noteable_id, :noteable_type, :commit_id)
end
def diff_view_line_data(line_code, position, line_type)
return if @diff_notes_disabled
use_legacy_diff_note = @use_legacy_diff_notes
# If the controller doesn't force the use of legacy diff notes, we
# determine this on a line-by-line basis by seeing if there already exist
......@@ -41,11 +49,8 @@ module NotesHelper
end
data = {
noteable_type: @comments_target[:noteable_type],
noteable_id: @comments_target[:noteable_id],
commit_id: @comments_target[:commit_id],
line_code: line_code,
line_type: line_type,
line_code: line_code
}
if use_legacy_diff_note
......@@ -73,11 +78,7 @@ module NotesHelper
)
end
button_tag(class: 'btn add-diff-note js-add-diff-note-button',
data: data,
title: 'Add a comment to this line') do
icon('comment-o')
end
data
end
def link_to_reply_discussion(note, line_type = nil)
......
......@@ -43,15 +43,15 @@ module SearchHelper
# Autocomplete results for internal help pages
def help_autocomplete
[
{ category: "Help", label: "API Help", url: help_page_path("api", "README") },
{ category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
{ category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
{ category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
{ category: "Help", label: "API Help", url: help_page_path("api/README") },
{ category: "Help", label: "Markdown Help", url: help_page_path("markdown/markdown") },
{ category: "Help", label: "Permissions Help", url: help_page_path("permissions/permissions") },
{ category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") },
]
end
......
module TodosHelper
def todos_pending_count
TodosFinder.new(current_user, state: :pending).execute.count
@todos_pending_count ||= TodosFinder.new(current_user, state: :pending).execute.count
end
def todos_done_count
TodosFinder.new(current_user, state: :done).execute.count
@todos_done_count ||= TodosFinder.new(current_user, state: :done).execute.count
end
def todo_action_name(todo)
......@@ -13,6 +13,7 @@ module TodosHelper
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
end
end
......
......@@ -28,4 +28,10 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
head :ok
end
# Send an entry from artifacts through Workhorse
def send_artifacts_entry(build, entry)
headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
head :ok
end
end
......@@ -29,6 +29,7 @@ module Emails
# used in notify layout
@target_url = @message.target_url
@project = Project.find(project_id)
@diff_notes_disabled = true
add_project_headers
headers['X-GitLab-Author'] = @message.author_username
......
......@@ -87,6 +87,12 @@ module Issuable
User.find(assignee_id_was).update_cache_counts if assignee_id_was
assignee.update_cache_counts if assignee
end
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
title_changed? || description_changed?
end
end
module ClassMethods
......
......@@ -19,7 +19,7 @@ class MergeRequest < ActiveRecord::Base
after_create :create_merge_request_diff, unless: :importing?
after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
......@@ -164,6 +164,10 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
def diffs(*args)
merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args)
end
def diff_size
merge_request_diff.size
end
......@@ -548,7 +552,13 @@ class MergeRequest < ActiveRecord::Base
end
def can_be_merged_by?(user)
::Gitlab::GitAccess.new(user, project, 'web').can_push_to_branch?(target_branch)
access = ::Gitlab::UserAccess.new(user, project: project)
access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch)
end
def can_be_merged_via_command_line_by?(user)
access = ::Gitlab::UserAccess.new(user, project: project)
access.can_push_to_branch?(target_branch)
end
def mergeable_ci_state?
......
......@@ -46,7 +46,8 @@ class MergeRequestDiff < ActiveRecord::Base
compare.diffs(options)
end
else
@diffs ||= load_diffs(st_diffs, options)
@diffs ||= {}
@diffs[options] ||= load_diffs(st_diffs, options)
end
end
......@@ -144,6 +145,12 @@ class MergeRequestDiff < ActiveRecord::Base
def load_diffs(raw, options)
if raw.respond_to?(:each)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
end
end
Gitlab::Git::DiffCollection.new(raw, options)
else
Gitlab::Git::DiffCollection.new([])
......
......@@ -10,6 +10,10 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer.
attr_accessor :note_html
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count
default_value_for :system, false
attr_mentionable :note, pipeline: :note
......@@ -193,7 +197,15 @@ class Note < ActiveRecord::Base
end
def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty?
cross_reference? && !has_referenced_mentionables?(user)
end
def has_referenced_mentionables?(user)
if user_visible_reference_count.present?
user_visible_reference_count > 0
else
referenced_mentionables(user).any?
end
end
def award_emoji?
......
......@@ -832,6 +832,10 @@ class Project < ActiveRecord::Base
protected_branches.matching(branch_name).any?(&:developers_can_push)
end
def developers_can_merge_to_protected_branch?(branch_name)
protected_branches.matching(branch_name).any?(&:developers_can_merge)
end
def forked?
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
......
......@@ -769,9 +769,9 @@ class Repository
end
end
def merge(user, source_sha, target_branch, options = {})
our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha)
def merge(user, merge_request, options = {})
our_commit = rugged.branches[merge_request.target_branch].target
their_commit = rugged.lookup(merge_request.diff_head_sha)
raise "Invalid merge target" if our_commit.nil?
raise "Invalid merge source" if their_commit.nil?
......@@ -779,14 +779,16 @@ class Repository
merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts?
commit_with_hooks(user, target_branch) do |ref|
commit_with_hooks(user, merge_request.target_branch) do |tmp_ref|
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
update_ref: ref
update_ref: tmp_ref
)
Rugged::Commit.create(rugged, actual_options)
commit_id = Rugged::Commit.create(rugged, actual_options)
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
end
......
......@@ -72,6 +72,19 @@ class SentNotification < ActiveRecord::Base
end
end
def position=(new_position)
if new_position.is_a?(String)
new_position = JSON.parse(new_position) rescue nil
end
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
end
super(new_position)
end
def to_param
self.reply_key
end
......
......@@ -3,12 +3,14 @@ class Todo < ActiveRecord::Base
MENTIONED = 2
BUILD_FAILED = 3
MARKED = 4
APPROVAL_REQUIRED = 5 # This is an EE-only feature
ACTION_NAMES = {
ASSIGNED => :assigned,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
MARKED => :marked
MARKED => :marked,
APPROVAL_REQUIRED => :approval_required
}
belongs_to :author, class_name: "User"
......
......@@ -87,7 +87,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
has_many :award_emoji, dependent: :destroy
#
# Validations
......
......@@ -23,7 +23,7 @@ module Commits
private
def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
......
......@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
def execute(source_project, source_branch, target_project, target_branch)
source_commit = source_project.commit(source_branch)
return unless source_commit
......
......@@ -42,7 +42,7 @@ module Files
end
def validate
allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
unless allowed
raise_error("You are not allowed to push into this branch")
......
......@@ -89,7 +89,8 @@ class GitPushService < BaseService
# Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
developers_can_merge = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push, developers_can_merge: developers_can_merge })
end
end
......
......@@ -34,7 +34,7 @@ module MergeRequests
committer: committer
}
commit_id = repository.merge(current_user, merge_request.diff_head_sha, merge_request.target_branch, options)
commit_id = repository.merge(current_user, merge_request, options)
merge_request.update(merge_commit_sha: commit_id)
rescue GitHooksService::PreReceiveError => e
merge_request.update(merge_error: e.message)
......@@ -43,6 +43,8 @@ module MergeRequests
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
false
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
end
def after_merge
......
......@@ -48,7 +48,7 @@ module MergeRequests
end
def force_push?
Gitlab::ForcePushCheck.force_push?(@project, @oldrev, @newrev)
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
end
# Refresh merge request diff if we push to source or target branch of merge request
......
......@@ -7,8 +7,6 @@
#
module Projects
class HousekeepingService < BaseService
include Gitlab::ShellAdapter
LEASE_TIMEOUT = 3600
class LeaseTaken < StandardError
......@@ -24,11 +22,7 @@ module Projects
def execute
raise LeaseTaken unless try_obtain_lease
GitlabShellOneShotWorker.perform_async(:gc, @project.repository_storage_path, @project.path_with_namespace)
ensure
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
update_pushes_since_gc(0)
end
execute_gitlab_shell_gc
end
def needed?
......@@ -36,18 +30,26 @@ module Projects
end
def increment!
if Gitlab::ExclusiveLease.new("project_housekeeping:increment!:#{@project.id}", timeout: 60).try_obtain
Gitlab::Metrics.measure(:increment_pushes_since_gc) do
update_pushes_since_gc(@project.pushes_since_gc + 1)
end
end
end
private
def execute_gitlab_shell_gc
GitGarbageCollectWorker.perform_async(@project.id)
ensure
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
update_pushes_since_gc(0)
end
end
def update_pushes_since_gc(new_value)
if Gitlab::ExclusiveLease.new("project_housekeeping:update_pushes_since_gc:#{project.id}", timeout: 60).try_obtain
@project.update_column(:pushes_since_gc, new_value)
end
end
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
......
......@@ -13,7 +13,7 @@
.col-sm-10
= f.text_area :description, class: "form-control", rows: 10
.hint
Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}.
Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}.
.form-group
= f.label :logo, class: 'control-label'
.col-sm-10
......
......@@ -38,11 +38,11 @@
= source
%span.help-block#import-sources-help
Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
= link_to "(?)", help_page_path("integration", "github")
= link_to "(?)", help_page_path("integration/github")
, Bitbucket
= link_to "(?)", help_page_path("integration", "bitbucket")
= link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com
= link_to "(?)", help_page_path("integration", "gitlab")
= link_to "(?)", help_page_path("integration/gitlab")
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
......
- project = build.project
%tr.build
%tr.build.commit
%td.status
= ci_status_with_icon(build.status)
%td.build-link
%td
.branch-commit
- if can?(current_user, :read_build, build.project)
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id}
%span.build-link ##{build.id}
- else
%strong Build ##{build.id}
%span.build-link ##{build.id}
- if build.stuck?
%i.fa.fa-warning.text-warning
%td
- if project
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
%td
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- else
.light none
= custom_icon("icon_commit")
%td
- if build.try(:runner)
= runner_link(build.runner)
- else
.light none
%td
#{build.stage} / #{build.name}
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id"
%td
.label-container
- if build.tags.any?
- build.tags.each do |tag|
%span.label.label-primary
......@@ -45,12 +32,28 @@
- if build.try(:allow_failure)
%span.label.label-danger allowed to fail
%td.duration
%td
- if project
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
%td
- if build.try(:runner)
= runner_link(build.runner)
- else
.light none
%td
#{build.stage} / #{build.name}
%td
- if build.duration
#{duration_in_words(build.finished_at, build.started_at)}
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(build.finished_at, build.started_at)
%td.timestamp
- if build.finished_at
%p.finished-at
= icon("calendar")
%span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage
......
......@@ -27,7 +27,7 @@
.row-content-block.second-block
#{(@scope || 'all').capitalize} builds
%ul.content-list
%ul.content-list.builds-content-list
- if @builds.blank?
%li
.nothing-here-block No builds to show
......@@ -37,15 +37,11 @@
%thead
%tr
%th Status
%th Build ID
%th Project
%th Commit
%th Ref
%th Project
%th Runner
%th Name
%th Tags
%th Duration
%th Finished at
%th
%th
- @builds.each do |build|
......
......@@ -14,7 +14,7 @@
.col-sm-10
%p.light
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh", "README")
= link_to "here", help_page_path("ssh/README")
= f.text_area :key, class: "form-control thin_area", rows: 5
.form-actions
......
......@@ -79,7 +79,7 @@
.panel-body.form-holder
%p.light
Read more about project permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
%strong= link_to "here", help_page_path("permissions/permissions"), class: "vlink"
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
......
......@@ -3,7 +3,7 @@
System hooks
%p.light
#{link_to "System hooks ", help_page_path("system_hooks", "system_hooks"), class: "vlink"} can be
#{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be
used for binding events when GitLab creates a User or Project.
%hr
......
......@@ -132,7 +132,7 @@
- else
passed.
= link_to icon('question-circle'), help_page_path('administration', 'repository_checks')
= link_to icon('question-circle'), help_page_path('administration/repository_checks')
.form-group
= f.submit 'Trigger repository check', class: 'btn btn-primary'
......
......@@ -9,14 +9,14 @@
%span
To do
%span.badge
= todos_pending_count
= number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
%li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do
%span
Done
%span.badge
= todos_done_count
= number_with_delimiter(todos_done_count)
.nav-controls
- if @todos.any?(&:pending?)
......
......@@ -3,4 +3,4 @@
%h3 Access Denied
%hr
%p You are not allowed to access this page.
%p Read more about project permissions #{link_to "here", help_page_path("permissions", "permissions"), class: "vlink"}
%p Read more about project permissions #{link_to "here", help_page_path("permissions/permissions"), class: "vlink"}
......@@ -12,7 +12,7 @@
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
%strong= link_to "here", help_page_path("permissions/permissions"), class: "vlink"
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
......@@ -6,7 +6,6 @@
.nav-controls
- if can?(current_user, :admin_milestones, @group)
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
= icon('plus')
New Milestone
.row-content-block
......
......@@ -7,7 +7,6 @@
- if can? current_user, :admin_group, @group
.controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
= icon('plus')
New Project
%ul.well-list
- @projects.each do |project|
......
......@@ -6,7 +6,6 @@
.cover-block.groups-cover-block
%div{ class: container_class }
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s70"
.group-info
.cover-title
......
- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize)
- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki
= markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
......@@ -549,4 +549,4 @@
%li wiki page
%li help page
You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown", "markdown")}.
You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}.
......@@ -38,6 +38,6 @@
As an administrator you may like to configure
- else
Consider asking your GitLab administrator to configure
= link_to 'GitHub integration', help_page_path("integration", "github")
= link_to 'GitHub integration', help_page_path("integration/github")
which will allow login via GitHub and allow importing projects without
generating a Personal Access Token.
= link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do
%span.sr-only Toggle navigation
= icon('bars')
.flash-container
.flash-container.flash-container-page
- if alert
.flash-alert
= alert
......
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
= render partial: 'layouts/collapse_button'
.sidebar-action-buttons
= link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
%span.sr-only Toggle navigation
= icon('bars')
= link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
%span.sr-only Toggle navigation pinning
= icon('thumb-tack')
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
......@@ -8,9 +15,6 @@
- else
= render 'layouts/nav/explore'
= link_to '#', class: "nav-header-btn text-center pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
%span.sr-only Toggle navigation pinning
= icon('thumb-tack')
- if defined?(nav) && nav
.layout-nav
.container-fluid
......
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
.icon-container
= navbar_icon('project')
%span
Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
.icon-container
= icon('bell fw')
%span
Todos
%span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
.icon-container
= navbar_icon('activity')
%span
Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
.icon-container
= navbar_icon('group')
%span
Groups
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
.icon-container
= navbar_icon('milestones')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
.icon-container
= navbar_icon('issues')
%span
Issues
%span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.icon-container
= navbar_icon('mr')
%span
Merge Requests
%span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
.icon-container
= icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
.icon-container
= icon('question-circle fw')
%span
Help
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
.icon-container
= icon('user fw')
%span
Profile Settings
......@@ -18,9 +18,9 @@
%span
Applications
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
Personal Access Tokens
Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
......
......@@ -11,7 +11,7 @@
Add an SSH key
%p.profile-settings-content
Before you can add an SSH key you need to
= link_to "generate it.", help_page_path("ssh", "README")
= link_to "generate it.", help_page_path("ssh/README")
= render 'form'
%hr
%h5
......
......@@ -43,12 +43,12 @@
.form-group
= f.label :dashboard, class: 'label-light' do
Default Dashboard
= link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
= link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank')
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group
= f.label :project_view, class: 'label-light' do
Project view
= link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
= link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank')
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
Choose what content you want to see on a project's home page.
......
......@@ -14,7 +14,7 @@
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
= raw @qr_code
......
......@@ -10,4 +10,4 @@
as administrator you need to configure
- else
ask your GitLab administrator to configure
== #{link_to 'OAuth integration', help_page_path("integration", "bitbucket")}.
== #{link_to 'OAuth integration', help_page_path("integration/bitbucket")}.
......@@ -4,7 +4,7 @@
- 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'
= 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
......
......@@ -10,4 +10,4 @@
as administrator you need to configure
- else
ask your GitLab administrator to configure
== #{link_to 'OAuth integration', help_page_path("integration", "gitlab")}.
== #{link_to 'OAuth integration', help_page_path("integration/gitlab")}.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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