Commit 7ae53600 authored by ZJ van de Weg's avatar ZJ van de Weg

Merge branch 'master' into awardables

parents 20e6e0db 318b2245
This diff is collapsed.
Please view this file on the master branch, on stable branches it's out of date.
v 8.8.0 (unreleased)
v 8.9.0 (unreleased)
- Redesign navigation for project pages
- Use gitlab-shell v3.0.0
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
v 8.8.2 (unreleased)
- Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources
- Fix Error 500 in CI charts by gracefully handling commits with no durations
- Fix concurrent request when updating build log in browser
v 8.8.1
- Add documentation for the "Health Check" feature
- Allow anonymous users to access a public project's pipelines
- Fix MySQL compatibility in zero downtime migrations helpers
- Fix the CI login to Container Registry (the gitlab-ci-token user)
v 8.8.0
- Implement GFM references for milestones (Alejandro Rodríguez)
- Snippets tab under user profile. !4001 (Long Nguyen)
- Fix error when using link to uploads in global snippets
- Fix Error 500 when attempting to retrieve project license when HEAD points to non-existent ref
- Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen)
- Use a case-insensitive comparison in sanitizing URI schemes
- Toggle sign-up confirmation emails in application settings
- Make it possible to prevent tagged runner from picking untagged jobs
- Added `InlineDiffFilter` to the markdown parser. (Adam Butler)
- Added inline diff styling for `change_title` system notes. (Adam Butler)
- Project#open_branches has been cleaned up and no longer loads entire records into memory.
- Escape HTML in commit titles in system note messages
- Fix scope used when accessing container registry
- Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios
- Improve multiple branch push performance by memoizing permission checking
- Log to application.log when an admin starts and stops impersonating a user
- Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi)
- Updated gitlab_git to 10.1.0
- GitAccess#protected_tag? no longer loads all tags just to check if a single one exists
- Reduce delay in destroying a project from 1-minute to immediately
......@@ -37,6 +61,7 @@ v 8.8.0 (unreleased)
- Added button to toggle whitespaces changes on diff view
- Backport GitHub Enterprise import support from EE
- Create tags using Rugged for performance reasons. !3745
- Allow guests to set notification level in projects
- API: Expose Issue#user_notes_count. !3126 (Anton Popov)
- Don't show forks button when user can't view forks
- Fix atom feed links and rendering
......@@ -44,7 +69,7 @@ v 8.8.0 (unreleased)
- Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes)
- Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724
- Added multiple colors for labels in dropdowns when dups happen.
- Always group commits by server timezone, not commit timestamp
- Show commits in the same order as `git log`
- Improve description for the Two-factor Authentication sign-in screen. (Connor Shea)
- API support for the 'since' and 'until' operators on commit requests (Paco Guzman)
- Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko)
......@@ -61,6 +86,9 @@ v 8.8.0 (unreleased)
- Import pull requests from GitHub where the source or target branches were removed
- All Grape API helpers are now instrumented
- Improve Issue formatting for the Slack Service (Jeroen van Baarsen)
- Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine)
- Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs)
- When creating a .gitignore file a dropdown with templates will be provided
v 8.7.6
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
......@@ -87,6 +115,7 @@ v 8.7.3
- Merge request widget displays TeamCity build state and code coverage correctly again.
- Fix the line code when importing PR review comments from GitHub. !4010
- Wikis are now initialized on legacy projects when checking repositories
- Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea)
v 8.7.2
- The "New Branch" button is now loaded asynchronously
......@@ -1520,20 +1549,17 @@ v 7.10.0
- Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
- Fix merge request comments on files with multiple commits
- Fix Resource Owner Password Authentication Flow
v 7.9.4
- Security: Fix project import URL regex to prevent arbitary local repos from being imported
- Fixed issue where only 25 commits would load in file listings
- Fix LDAP identities after config update
v 7.9.3
- Contains no changes
- Add icons to Add dropdown items.
- Allow admin to create public deploy keys that are accessible to any project.
- Warn when gitlab-shell version doesn't match requirement.
- Skip email confirmation when set by admin or via LDAP.
- Only allow users to reference groups, projects, issues, MRs, commits they have access to.
v 7.9.4
- Security: Fix project import URL regex to prevent arbitary local repos from being imported
- Fixed issue where only 25 commits would load in file listings
- Fix LDAP identities after config update
v 7.9.3
- Contains no changes
......
......@@ -293,9 +293,10 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.38.0', require: false
gem 'rubocop', '~> 0.40.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.11.0', require: false
gem 'flog', require: false
gem 'flay', require: false
......@@ -325,7 +326,7 @@ gem "mail_room", "~> 0.7"
gem 'email_reply_parser', '~> 0.5.8'
## CI
gem 'activerecord-session_store', '~> 0.1.0'
gem 'activerecord-session_store', '~> 1.0.0'
gem "nested_form", '~> 0.3.2'
# OAuth
......
......@@ -33,10 +33,12 @@ GEM
activemodel (= 4.2.6)
activesupport (= 4.2.6)
arel (~> 6.0)
activerecord-session_store (0.1.2)
actionpack (>= 4.0.0, < 5)
activerecord (>= 4.0.0, < 5)
railties (>= 4.0.0, < 5)
activerecord-session_store (1.0.0)
actionpack (>= 4.0, < 5.1)
activerecord (>= 4.0, < 5.1)
multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1)
activesupport (4.2.6)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
......@@ -549,7 +551,7 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
parser (2.3.0.6)
parser (2.3.1.0)
ast (~> 2.2)
pg (0.18.4)
poltergeist (1.9.0)
......@@ -684,15 +686,17 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.4.1)
rubocop (0.38.0)
parser (>= 2.3.0.6, < 3.0)
rubocop (0.40.0)
parser (>= 2.3.1.0, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.5.0)
rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
ruby-progressbar (1.8.1)
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
......@@ -839,7 +843,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.0.2)
unicode-display_width (1.0.5)
unicorn (4.9.0)
kgio (~> 2.6)
rack
......@@ -883,7 +887,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.2.9)
ace-rails-ap (~> 4.0.2)
activerecord-session_store (~> 0.1.0)
activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
......@@ -1013,7 +1017,8 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.4.0)
rspec-retry
rubocop (~> 0.38.0)
rubocop (~> 0.40.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
......@@ -1058,4 +1063,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.12.3
1.12.4
8.8.0-pre
8.9.0-pre
@Api =
groups_path: "/api/:version/groups.json"
group_path: "/api/:version/groups/:id.json"
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
license_path: "/api/:version/licenses/:key"
groupsPath: "/api/:version/groups.json"
groupPath: "/api/:version/groups/:id.json"
namespacesPath: "/api/:version/namespaces.json"
groupProjectsPath: "/api/:version/groups/:id/projects.json"
projectsPath: "/api/:version/projects.json"
labelsPath: "/api/:version/projects/:id/labels"
licensePath: "/api/:version/licenses/:key"
gitignorePath: "/api/:version/gitignores/:key"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
url = Api.buildUrl(Api.groupPath)
url = url.replace(':id', group_id)
$.ajax(
......@@ -22,7 +23,7 @@
# Return groups list. Filtered by query
# Only active groups retrieved
groups: (query, skip_ldap, callback) ->
url = Api.buildUrl(Api.groups_path)
url = Api.buildUrl(Api.groupsPath)
$.ajax(
url: url
......@@ -36,7 +37,7 @@
# Return namespaces list. Filtered by query
namespaces: (query, callback) ->
url = Api.buildUrl(Api.namespaces_path)
url = Api.buildUrl(Api.namespacesPath)
$.ajax(
url: url
......@@ -50,7 +51,7 @@
# Return projects list. Filtered by query
projects: (query, order, callback) ->
url = Api.buildUrl(Api.projects_path)
url = Api.buildUrl(Api.projectsPath)
$.ajax(
url: url
......@@ -64,7 +65,7 @@
callback(projects)
newLabel: (project_id, data, callback) ->
url = Api.buildUrl(Api.labels_path)
url = Api.buildUrl(Api.labelsPath)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
......@@ -80,7 +81,7 @@
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
url = Api.buildUrl(Api.groupProjectsPath)
url = url.replace(':id', group_id)
$.ajax(
......@@ -95,7 +96,7 @@
# Return text for a specific license
licenseText: (key, data, callback) ->
url = Api.buildUrl(Api.license_path).replace(':key', key)
url = Api.buildUrl(Api.licensePath).replace(':key', key)
$.ajax(
url: url
......@@ -103,6 +104,12 @@
).done (license) ->
callback(license)
gitignoreText: (key, callback) ->
url = Api.buildUrl(Api.gitignorePath).replace(':key', key)
$.get url, (gitignore) ->
callback(gitignore)
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
class @BlobGitignoreSelector
constructor: (opts) ->
{
@dropdown
@editor
@$wrapper = @dropdown.closest('.gitignore-selector')
@$filenameInput = $('#file_name')
@data = @dropdown.data('filenames')
} = opts
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (gitignore) ->
gitignore.name
)
@toggleGitignoreSelector()
@bindEvents()
bindEvents: ->
@$filenameInput
.on 'keyup blur', (e) =>
@toggleGitignoreSelector()
toggleGitignoreSelector: ->
filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
@$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
onClick: (item, el, e) =>
e.preventDefault()
@requestIgnoreFile(item.name)
requestIgnoreFile: (name) ->
Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
requestIgnoreFileSuccess: (gitignore) ->
@editor.setValue(gitignore.content, 1)
@editor.focus()
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
dropdown: $dropdown,
editor: @editor
)
......@@ -13,6 +13,7 @@ class @EditBlob
@initModePanesAndLinks()
new BlobLicenseSelector(@editor)
new BlobGitignoreSelectors(editor: @editor)
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
......
......@@ -28,12 +28,15 @@ class CiBuild
#
CiBuild.interval = setInterval =>
if window.location.href.split("#").first() is build_url
last_state = @state
$.ajax
url: build_url + "/trace.json?state=" + encodeURIComponent(@state)
dataType: "json"
success: (log) =>
@state = log.state
if log.status is "running"
return unless last_state is @state
if log.state and log.status is "running"
@state = log.state
if log.append
$('.fa-refresh').before log.html
else
......
......@@ -121,7 +121,7 @@ class Dispatcher
new UsersSelect()
when 'projects'
new NamespaceSelect()
when 'dashboard'
when 'dashboard', 'root'
shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles'
new Profile()
......
......@@ -11,6 +11,7 @@ class @DueDateSelect
$block = $dropdown.closest('.block')
$selectbox = $dropdown.closest('.selectbox')
$value = $block.find('.value')
$valueContent = $block.find('.value-content')
$sidebarValue = $('.js-due-date-sidebar-value', $block)
fieldName = $dropdown.data('field-name')
......@@ -23,11 +24,15 @@ class @DueDateSelect
$value.removeAttr('style')
)
addDueDate = ->
addDueDate = (isDropdown) ->
# Create the post date
value = $("input[name='#{fieldName}']").val()
date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date
if value isnt ''
date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date
else
mediumDate = 'None'
data = {}
data[abilityName] = {}
......@@ -39,23 +44,35 @@ class @DueDateSelect
data: data
beforeSend: ->
$loading.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
$selectbox.hide()
if isDropdown
$dropdown.trigger('loading.gl.dropdown')
$selectbox.hide()
$value.removeAttr('style')
$value.html(mediumDate)
$valueContent.html(mediumDate)
$sidebarValue.html(mediumDate)
if value isnt ''
$('.js-remove-due-date-holder').removeClass 'hidden'
else
$('.js-remove-due-date-holder').addClass 'hidden'
).done (data) ->
$dropdown.trigger('loaded.gl.dropdown')
$dropdown.dropdown('toggle')
if isDropdown
$dropdown.trigger('loaded.gl.dropdown')
$dropdown.dropdown('toggle')
$loading.fadeOut()
$block.on 'click', '.js-remove-due-date', (e) ->
e.preventDefault()
$("input[name='#{fieldName}']").val ''
addDueDate(false)
$datePicker.datepicker(
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='#{fieldName}']").val()
altField: "input[name='#{fieldName}']"
onSelect: ->
addDueDate()
addDueDate(true)
)
$(document)
......
......@@ -18,6 +18,10 @@ GitLab.GfmAutoComplete =
Issues:
template: '<li><small>${id}</small> ${title}</li>'
# Milestones
Milestones:
template: '<li>${title}</li>'
# Add GFM auto-completion to all input fields, that accept GFM input.
setup: (wrap) ->
@input = $('.js-gfm-input')
......@@ -81,6 +85,19 @@ GitLab.GfmAutoComplete =
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
@input.atwho
at: '%'
alias: 'milestones'
searchKey: 'search'
displayTpl: @Milestones.template
insertTpl: '${atwho-at}"${title}"'
callbacks:
beforeSave: (milestones) ->
$.map milestones, (m) ->
id: m.iid
title: sanitize(m.title)
search: "#{m.title}"
@input.atwho
at: '!'
alias: 'mergerequests'
......@@ -105,6 +122,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', '@', data.members
# load issues
@input.atwho 'load', 'issues', data.issues
# load milestones
@input.atwho 'load', 'milestones', data.milestones
# load merge requests
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
......
......@@ -60,9 +60,36 @@ class GitLabDropdownFilter
results = data
if search_text isnt ''
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
# When data is an array of objects therefore [object Array] e.g.
# [
# { prop: 'foo' },
# { prop: 'baz' }
# ]
if _.isArray(data)
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
else
# If data is grouped therefore an [object Object]. e.g.
# {
# groupName1: [
# { prop: 'foo' },
# { prop: 'baz' }
# ],
# groupName2: [
# { prop: 'abc' },
# { prop: 'def' }
# ]
# }
if gl.utils.isObject data
results = {}
for key, group of data
tmp = fuzzaldrinPlus.filter(group, search_text,
key: @options.keys
)
if tmp.length
results[key] = tmp.map (item) -> item
@options.callback results
else
......@@ -141,8 +168,9 @@ class GitLabDropdown
searchFields = if @options.search then @options.search.fields else [];
if @options.data
# If data is an array
if _.isArray @options.data
# If we provided data
# data could be an array of objects or a group of arrays
if _.isObject(@options.data) and not _.isFunction(@options.data)
@fullData = @options.data
@parseData @options.data
else
......@@ -230,19 +258,33 @@ class GitLabDropdown
parseData: (data) ->
@renderedData = data
# Render each row
html = $.map data, (obj) =>
return @renderItem(obj)
if @options.filterable and data.length is 0
# render no matching results
html = [@noResults()]
else
# Handle array groups
if gl.utils.isObject data
html = []
for name, groupData of data
# Add header for each group
html.push(@renderItem(header: name, name))
@renderData(groupData, name)
.map (item) ->
html.push item
else
# Render each row
html = @renderData(data)
# Render the full menu
full_html = @renderMenu(html.join(""))
@appendMenu(full_html)
renderData: (data, group = false) ->
data.map (obj, index) =>
return @renderItem(obj, group, index)
shouldPropagate: (e) =>
if @options.multiSelect
$target = $(e.target)
......@@ -299,11 +341,10 @@ class GitLabDropdown
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
$(selector, @dropdown).html html
# Render the row
renderItem: (data) ->
renderItem: (data, group = false, index = false) ->
html = ""
# Divider
......@@ -346,8 +387,13 @@ class GitLabDropdown
if @highlight
text = @highlightTextMatches(text, @filterInput.val())
if group
groupAttrs = "data-group='#{group}' data-index='#{index}'"
else
groupAttrs = ''
html = "<li>
<a href='#{url}' class='#{cssClass}'>
<a href='#{url}' #{groupAttrs} class='#{cssClass}'>
#{text}
</a>
</li>"
......@@ -377,9 +423,15 @@ class GitLabDropdown
rowClicked: (el) ->
fieldName = @options.fieldName
selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
groupName = el.data('group')
if groupName
selectedIndex = el.data('index')
selectedObject = @renderedData[groupName][selectedIndex]
else
selectedIndex = el.closest('li').index()
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS)
......@@ -460,7 +512,7 @@ class GitLabDropdown
return false
if currentKeyCode is 13
@selectRowAtIndex currentIndex
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex
removeArrayKeyEvent: ->
$('body').off 'keydown'
......
......@@ -20,6 +20,15 @@ class @IssuableForm
@initWip()
$issuableDueDate = $('#issuable-due-date')
if $issuableDueDate.length
$('.datepicker').datepicker(
dateFormat: 'yy-mm-dd',
onSelect: (dateText, inst) ->
$issuableDueDate.val dateText
).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())
initAutosave: ->
new Autosave @titleField, [
document.location.pathname,
......
((w) ->
w.gl ?= {}
w.gl.utils ?= {}
w.gl.utils.isObject = (obj) ->
obj? and (obj.constructor is Object)
) window
......@@ -75,6 +75,9 @@ class @MergeRequestTabs
@loadDiff($target.attr('href'))
if bp? and bp.getBreakpointSize() isnt 'lg'
@shrinkView()
navBarHeight = $('.navbar-gitlab').outerHeight()
$.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight)
else if action == 'builds'
@loadBuilds($target.attr('href'))
@expandView()
......
......@@ -113,7 +113,7 @@ class @MergeRequestWidget
switch state
when "failed", "canceled", "not_found"
@setMergeButtonClass('btn-danger')
when "running", "pending"
when "running"
@setMergeButtonClass('btn-warning')
when "success"
@setMergeButtonClass('btn-create')
......@@ -126,6 +126,6 @@ class @MergeRequestWidget
$('.ci_widget:visible .ci-coverage').text(text)
setMergeButtonClass: (css_class) ->
$('.accept_merge_request')
$('.js-merge-button')
.removeClass('btn-danger btn-warning btn-create')
.addClass(css_class)
......@@ -3,10 +3,10 @@
class @ShortcutsDashboardNavigation extends Shortcuts
constructor: ->
super()
Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-activity'))
Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-issues'))
Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-merge_requests'))
Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-projects'))
Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity'))
Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues'))
Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'))
Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'))
@findAndFollowLink: (selector) ->
link = $(selector).attr('href')
......
......@@ -10,7 +10,6 @@
*= require dropzone/basic
*= require cal-heatmap
*= require cropper.css
*= require animate
*/
/*
......
......@@ -5,6 +5,7 @@
@import 'framework/tw_bootstrap';
@import "framework/layout";
@import "framework/animations.scss";
@import "framework/avatar.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
......
// This file is based off animate.css 3.5.1, available here:
// https://github.com/daneden/animate.css/blob/3.5.1/animate.css
//
// animate.css - http://daneden.me/animate
// Version - 3.5.1
// Licensed under the MIT license - http://opensource.org/licenses/MIT
//
// Copyright (c) 2016 Daniel Eden
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.animated.flipOutX,
.animated.flipOutY,
.animated.bounceIn,
.animated.bounceOut {
-webkit-animation-duration: .75s;
animation-duration: .75s;
}
@-webkit-keyframes pulse {
from {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
50% {
-webkit-transform: scale3d(1.05, 1.05, 1.05);
transform: scale3d(1.05, 1.05, 1.05);
}
to {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes pulse {
from {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
50% {
-webkit-transform: scale3d(1.05, 1.05, 1.05);
transform: scale3d(1.05, 1.05, 1.05);
}
to {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
.pulse {
-webkit-animation-name: pulse;
animation-name: pulse;
}
......@@ -45,6 +45,7 @@
&.s32 { font-size: 20px; line-height: 32px; }
&.s40 { font-size: 16px; line-height: 40px; }
&.s60 { font-size: 32px; line-height: 60px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 90px; }
&.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 140px; }
......
......@@ -24,8 +24,8 @@
background-color: $background-color;
padding: $gl-padding;
margin-bottom: 0;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
color: $gl-gray;
&.oneline-block {
......@@ -110,9 +110,9 @@
.cover-title {
color: $gl-header-color;
margin: 0;
font-size: 23px;
font-size: 24px;
font-weight: normal;
margin: 16px 0 5px;
margin-bottom: 5px;
color: #4c4e54;
font-size: 23px;
line-height: 1.1;
......@@ -137,7 +137,6 @@
}
.cover-desc {
padding: 0 $gl-padding 3px;
color: $gl-text-color;
&.username:last-child {
......@@ -205,7 +204,7 @@
.content-block {
padding: $gl-padding 0;
border-bottom: 1px solid $border-color;
border-bottom: 1px solid $white-dark;
&.oneline-block {
line-height: 36px;
......
......@@ -162,6 +162,10 @@
}
}
.dropdown-menu-full-width {
width: 100%;
}
.dropdown-menu-paging {
.dropdown-page-two,
.dropdown-menu-back {
......
......@@ -5,6 +5,10 @@
.file-holder {
border: 1px solid $border-color;
&.file-holder-no-border {
border: 0;
}
&.readme-holder {
margin: $gl-padding-top 0;
}
......@@ -23,8 +27,17 @@
word-wrap: break-word;
border-radius: 3px 3px 0 0;
&.file-title-clear {
padding-left: 0;
padding-right: 0;
background-color: transparent;
.file-actions {
right: 0;
}
}
.file-actions {
float: right;
position: absolute;
top: 5px;
right: 15px;
......@@ -36,22 +49,6 @@
}
}
.filename {
&.old {
display: inline-block;
span.idiff {
background-color: #f8cbcb;
}
}
&.new {
display: inline-block;
span.idiff {
background-color: #a6f3a6;
}
}
}
a:not(.btn) {
color: $gl-dark-link-color;
}
......
......@@ -28,10 +28,6 @@ input[type='text'].danger {
}
label {
&.control-label {
@extend .col-sm-2;
}
&.inline-label {
margin: 0;
}
......@@ -41,6 +37,10 @@ label {
}
}
.control-label {
@extend .col-sm-2;
}
.inline-input-group {
width: 250px;
}
......
......@@ -48,10 +48,6 @@
display: block;
}
.project-home-desc {
font-size: 21px;
}
.project-repo-buttons,
.git-clone-holder {
display: none;
......
......@@ -196,7 +196,7 @@
position: fixed;
top: $header-height;
width: 100%;
z-index: 1;
z-index: 3;
background: $background-color;
border-bottom: 1px solid $border-color;
transition-duration: .3s;
......@@ -238,6 +238,10 @@
@media (max-width: $screen-xs-min) {
margin-left: 0;
}
li.active {
font-weight: bold;
}
}
}
......@@ -246,6 +250,11 @@
height: 51px;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
}
li {
......@@ -279,4 +288,8 @@
margin-top: 96px;
}
}
.right-sidebar {
top: ($header-height * 2) + 2;
}
}
......@@ -269,3 +269,11 @@ h1, h2, h3, h4 {
text-align: right;
}
}
.idiff.deletion {
background: $line-removed-dark;
}
.idiff.addition {
background: $line-added-dark;
}
......@@ -12,7 +12,7 @@ $gutter_inner_width: 258px;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
$table-border-color: #ececec;
$table-border-color: #f0f0f0;
$background-color: #fafafa;
/*
......@@ -119,8 +119,8 @@ $border-white-light: #f1f2f4;
$border-white-normal: #d6dae2;
$border-white-dark: #c6cacf;
$border-gray-light: rgba(0, 0, 0, 0.06);
$border-gray-normal: rgba(0, 0, 0, 0.10);;
$border-gray-light: #dcdcdc;
$border-gray-normal: rgba(0, 0, 0, 0.10);
$border-gray-dark: #c6cacf;
$border-green-light: #2faa60;
......@@ -178,6 +178,7 @@ $table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
/*
* Fonts
*/
......
@import "framework/variables";
table.code {
width: 100%;
font-family: monospace;
border: none;
border-collapse: separate;
margin: 0;
padding: 0;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
td {
line-height: $code_line_height;
font-family: monospace;
font-size: $code_font_size;
}
td.diff-line-num {
margin: 0;
padding: 0;
border: none;
background: $background-color;
color: rgba(0, 0, 0, 0.3);
padding: 0 5px;
border-right: 1px solid $border-color;
text-align: right;
min-width: 35px;
max-width: 50px;
width: 35px;
}
td.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
border: none;
white-space: pre;
}
}
@import "highlight/white";
......@@ -23,7 +23,7 @@
.file-title {
@extend .monospace;
line-height: 42px;
line-height: 35px;
padding-top: 7px;
padding-bottom: 7px;
......@@ -43,7 +43,7 @@
.editor-file-name {
@extend .monospace;
float: left;
margin-right: 10px;
}
......@@ -59,7 +59,22 @@
}
.encoding-selector,
.license-selector {
.license-selector,
.gitignore-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
}
.gitignore-selector {
.dropdown {
line-height: 21px;
}
.dropdown-menu-toggle {
vertical-align: top;
width: 220px;
}
}
}
......@@ -150,6 +150,10 @@
font-weight: 600;
}
.light {
font-weight: normal;
}
.sidebar-collapsed-icon {
display: none;
}
......
.pipeline-stage {
overflow: hidden;
text-overflow: ellipsis;
}
......@@ -134,14 +134,6 @@
}
}
.change-username-title {
color: $gl-warning;
}
.remove-account-title {
color: $gl-danger;
}
.provider-btn-group {
display: inline-block;
margin-right: 10px;
......
......@@ -10,7 +10,7 @@
margin-bottom: 0;
}
.new_project,
.edit_project {
.edit-project {
fieldset.features {
.control-label {
font-weight: normal;
......@@ -26,8 +26,13 @@
}
.project-home-panel {
padding-bottom: 40px;
border-bottom: 1px solid $border-color;
background: $white-light;
text-align: left;
padding: 24px 0;
.container-fluid {
position: relative;
}
.cover-controls {
.project-settings-dropdown {
......@@ -43,21 +48,55 @@
}
}
.project-identicon-holder {
margin-bottom: 16px;
.cover-title {
margin-bottom: 0;
}
.project-image-container {
@include make-sm-column(1);
max-width: 86px;
min-width: 86px;
padding-right: 0;
margin: 11px 0;
.avatar, .identicon {
margin: 0 auto;
float: none;
@media (max-width: $screen-md-max) {
padding-left: 0;
margin: 0 0 10px;
max-width: none;
min-width: none;
.avatar.s70 {
margin: auto;
}
}
}
.identicon {
@include border-radius(50%);
.project-info {
@include make-sm-column(10);
h1 {
font-size: 24px;
font-weight: normal;
margin: 0;
}
.project-home-desc {
p {
margin: 0;
}
}
}
.identicon {
float: left;
@include border-radius(50%);
}
.avatar {
float: none;
}
.notifications-btn {
margin-top: -28px;
.fa-bell {
margin-right: 6px;
......@@ -69,28 +108,45 @@
}
.project-repo-buttons {
margin-top: 20px;
margin-bottom: 0;
font-size: 0;
.count-buttons {
display: block;
margin-bottom: 20px;
}
.btn {
@include btn-gray;
padding: 3px 10px;
text-transform: none;
background-color: $background-color;
.clone-row {
.split-repo-buttons,
.project-clone-holder {
display: inline-block;
.fa {
color: $layout-link-gray;
}
.split-repo-buttons {
margin: 0 12px;
.fa-caret-down {
margin-left: 3px;
}
}
.btn {
@include btn-gray;
text-transform: none;
.btn-group:not(:first-child):not(:last-child) > .btn {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
form {
margin-left: 10px;
}
.count-buttons {
display: inline-block;
vertical-align: top;
margin-top: 16px;
}
.project-clone-holder {
display: inline-block;
margin-top: 16px;
input {
height: 29px;
}
}
.count-with-arrow {
......@@ -140,14 +196,18 @@
line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 10px 14px;
padding: 7px 14px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
white-space: nowrap;
margin: 0 11px 0 4px;
margin: 0 10px 0 4px;
a {
color: inherit;
}
&:hover {
background: #fff;
......@@ -155,13 +215,37 @@
}
}
}
.project-right-buttons {
position: absolute;
right: 16px;
bottom: 0;
.btn {
padding: 3px 10px;
background-color: $background-color;
}
@media (max-width: 1304px) {
top: 0;
}
}
@media (max-width: $screen-md-max) {
text-align: center;
.project-info,
.project-image-container {
width: 100%;
}
}
}
.split-one {
display: inline-table;
margin-right: 12px;
a {
> a {
margin: -1px;
}
}
......@@ -285,11 +369,11 @@ a.deploy-project-label {
}
.project-stats {
text-align: center;
margin-top: $gl-padding;
margin-bottom: 0;
padding-top: 10px;
padding-bottom: 4px;
padding: 16px 0;
background-color: $white-light;
font-size: 0;
ul.nav {
display: inline-block;
......@@ -300,12 +384,11 @@ a.deploy-project-label {
}
.nav > li > a {
@include btn-default;
@include btn-gray;
background-color: transparent;
border: 1px solid #f7f8fa;
margin-left: 12px;
margin-right: 12px;
padding: 0 10px;
font-size: 15px;
color: $notes-light-color;
}
li {
......@@ -325,6 +408,10 @@ a.deploy-project-label {
background-color: #f0f2f5;
}
}
&.row-content-block.second-block {
margin-top: 0;
}
}
pre.light-well {
......@@ -442,9 +529,14 @@ pre.light-well {
border-top: 0;
.edit-project-readme {
z-index: 100;
z-index: 2;
position: relative;
}
.wiki h1 {
border-bottom: none;
padding: 0;
}
}
.git-clone-holder {
......
......@@ -12,3 +12,11 @@
border: 1px solid $warning-message-border;
border-radius: $border-radius-base;
}
.warning-title {
color: $gl-warning;
}
.danger-title {
color: $gl-danger;
}
......@@ -16,19 +16,6 @@
}
}
.snippet-box {
@include border-radius(2px);
display: block;
float: left;
padding: 0 $gl-padding;
font-weight: normal;
margin-right: 10px;
font-size: $gl-font-size;
border: 1px solid;
line-height: 32px;
}
.markdown-snippet-copy {
position: fixed;
top: -10px;
......@@ -36,3 +23,34 @@
max-height: 0;
max-width: 0;
}
.file-holder.snippet-file-content {
padding-bottom: $gl-padding;
border-bottom: 1px solid $border-color;
.file-title {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
}
.file-actions {
top: 12px;
}
.file-content {
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
border-bottom: 1px solid $border-color;
}
}
.snippet-title {
font-size: 24px;
font-weight: normal;
}
.snippet-actions {
@media (min-width: $screen-sm-min) {
float: right;
}
}
......@@ -9,23 +9,18 @@ class Admin::RunnersController < Admin::ApplicationController
end
def show
@builds = @runner.builds.order('id DESC').first(30)
@projects =
if params[:search].present?
::Project.search(params[:search])
else
Project.all
end
@projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
assign_builds_and_projects
end
def update
@runner.update_attributes(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
if @runner.update_attributes(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
end
else
assign_builds_and_projects
render 'show'
end
end
......@@ -60,4 +55,16 @@ class Admin::RunnersController < Admin::ApplicationController
def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
def assign_builds_and_projects
@builds = runner.builds.order('id DESC').first(30)
@projects =
if params[:search].present?
::Project.search(params[:search])
else
Project.all
end
@projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
end
end
......@@ -36,7 +36,7 @@ class JwtController < ApplicationController
end
def authenticate_project(login, password)
if login == 'gitlab_ci_token'
if login == 'gitlab-ci-token'
Project.find_by(builds_enabled: true, runners_token: password)
end
end
......
......@@ -336,7 +336,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :task_num, label_ids: []
:state_event, :description, :task_num, :force_remove_source_branch,
label_ids: []
)
end
......
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
before_action :commit, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
def index
@scope = params[:scope]
all_pipelines = project.ci_commits
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
@pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
end
def new
@pipeline = project.ci_commits.new(ref: @project.default_branch)
end
def create
@pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
unless @pipeline.persisted?
render 'new'
return
end
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
end
def show
end
def retry
pipeline.retry_failed
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
def cancel
pipeline.cancel_running
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
private
def create_params
params.require(:pipeline).permit(:ref)
end
def pipeline
@pipeline ||= project.ci_commits.find_by!(id: params[:id])
end
def commit
@commit ||= @pipeline.commit_data
end
end
......@@ -20,7 +20,7 @@ class Projects::RunnersController < Projects::ApplicationController
if @runner.update_attributes(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
render 'edit'
end
end
......
......@@ -3,20 +3,44 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings'
def index
@variable = Ci::Variable.new
end
def show
@variable = @project.variables.find(params[:id])
end
def update
if project.update_attributes(project_params)
@variable = @project.variables.find(params[:id])
if @variable.update_attributes(project_params)
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully updated.'
else
render action: "show"
end
end
def create
@variable = Ci::Variable.new(project_params)
if @variable.valid? && @project.variables << @variable
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
else
render action: 'show'
render action: "index"
end
end
def destroy
@key = @project.variables.find(params[:id])
@key.destroy
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.'
end
private
def project_params
params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
params.require(:variable).permit([:id, :key, :value, :_destroy])
end
end
......@@ -101,13 +101,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
if current_user
@membership = @project.team.find_member(current_user.id)
if @membership
@notification_setting = current_user.notification_settings_for(@project)
end
end
@notification_setting = current_user.notification_settings_for(@project) if current_user
if @project.repository_exists?
if @project.empty_repo?
......@@ -147,6 +141,7 @@ class ProjectsController < Projects::ApplicationController
@suggestions = {
emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
members: participants
}
......
......@@ -38,7 +38,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def after_sign_up_path_for(user)
user.confirmed_at.present? ? dashboard_projects_path : users_almost_there_path
user.confirmed? ? dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(_resource)
......
class PipelinesFinder
attr_reader :project
def initialize(project)
@project = project
end
def execute(pipelines, scope)
case scope
when 'running'
pipelines.running_or_pending
when 'branches'
from_ids(pipelines, ids_for_ref(pipelines, branches))
when 'tags'
from_ids(pipelines, ids_for_ref(pipelines, tags))
else
pipelines
end
end
private
def ids_for_ref(pipelines, refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
def from_ids(pipelines, ids)
pipelines.unscoped.where(id: ids)
end
def branches
project.repository.branches.map(&:name)
end
def tags
project.repository.tags.map(&:name)
end
end
......@@ -36,7 +36,7 @@ class TodosFinder
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
end
def action_id
......
......@@ -110,8 +110,7 @@ module ApplicationHelper
]
# If reference is commit id - we should add it to branch/tag selectbox
if(@ref && !options.flatten.include?(@ref) &&
@ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/
options << ['Commit', [@ref]]
end
......
......@@ -184,4 +184,14 @@ module BlobHelper
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
}
end
def gitignore_names
return @gitignore_names if defined?(@gitignore_names)
@gitignore_names = {
Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } },
# Note that the key here doesn't cover it really
Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } }
}
end
end
......@@ -38,19 +38,30 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
def render_ci_status(ci_commit, tooltip_placement: 'auto left')
# TODO: split this method into
# - render_commit_status
# - render_pipeline_status
link_to ci_icon_for_status(ci_commit.status),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_label_for_status(ci_commit.status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
project = pipeline.project
path = namespace_project_pipeline_path(project.namespace, project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
end
def no_runners_for_project?(project)
project.runners.blank? &&
Ci::Runner.shared.blank?
end
private
def render_status_with_link(type, status, path, tooltip_placement)
link_to ci_icon_for_status(status),
path,
class: "ci-status-link ci-status-icon-#{status.dasherize}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
end
......@@ -2,8 +2,8 @@ module DiffHelper
def mark_inline_diffs(old_line, new_line)
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs)
marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs)
marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion)
marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition)
[marked_old_line, marked_new_line]
end
......
......@@ -32,12 +32,6 @@ module EmailsHelper
nil
end
def color_email_diff(diffcontent)
formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
lexer = Rouge::Lexers::Diff
raw formatter.format(lexer.lex(diffcontent))
end
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
......
......@@ -13,7 +13,7 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
escaped_body = if body =~ /\A\<img/
escaped_body = if body.start_with?('<img')
body
else
escape_once(body)
......
......@@ -124,11 +124,7 @@ module ProjectsHelper
end
def license_short_name(project)
no_license_key = project.repository.license_key.nil? ||
# Back-compat if cache contains 'no-license', can be removed in a few weeks
project.repository.license_key == 'no-license'
return 'LICENSE' if no_license_key
return 'LICENSE' if project.repository.license_key.nil?
license = Licensee::License.new(project.repository.license_key)
......@@ -148,6 +144,10 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_build, project)
nav_tabs << :builds
end
......
......@@ -95,7 +95,9 @@ module TabHelper
end
def project_tab_class
return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
if controller.controller_path.start_with?('projects')
return 'active'
end
if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
"active"
......@@ -112,7 +114,7 @@ module TabHelper
end
def profile_tab_class
if controller.controller_path =~ /\Aprofiles/
if controller.controller_path.start_with?('profiles')
return 'active'
end
......
......@@ -11,6 +11,7 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
end
end
......@@ -28,8 +29,11 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
polymorphic_path([todo.project.namespace.becomes(Namespace),
todo.project, todo.target], anchor: anchor)
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
path.unshift(:builds) if todo.build_failed?
polymorphic_path(path, anchor: anchor)
end
end
......
......@@ -65,7 +65,8 @@ module Emails
# used in notify layout
@target_url = @message.target_url
@project = Project.find project_id
@project = Project.find(project_id)
@diff_notes_disabled = true
add_project_headers
headers['X-GitLab-Author'] = @message.author_username
......
......@@ -10,6 +10,8 @@ class Notify < BaseMailer
include Emails::Builds
add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper
def test_email(recipient_email, subject, body)
......
......@@ -60,6 +60,7 @@ class Ability
:read_project_member,
:read_merge_request,
:read_note,
:read_pipeline,
:read_commit_status,
:read_container_image,
:download_code
......@@ -205,6 +206,7 @@ class Ability
:read_commit_status,
:read_build,
:read_container_image,
:read_pipeline,
]
end
......@@ -216,6 +218,8 @@ class Ability
:update_commit_status,
:create_build,
:update_build,
:create_pipeline,
:update_pipeline,
:create_merge_request,
:create_wiki,
:push_code,
......@@ -248,6 +252,7 @@ class Ability
:admin_commit_status,
:admin_build,
:admin_container_image,
:admin_pipeline
]
end
......@@ -290,6 +295,7 @@ class Ability
unless project.builds_enabled
rules += named_abilities('build')
rules += named_abilities('pipeline')
end
unless project.container_registry_enabled
......
......@@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :restricted_visibility_levels
serialize :import_sources
serialize :disabled_oauth_sign_in_sources
serialize :disabled_oauth_sign_in_sources, Array
serialize :restricted_signup_domains, Array
attr_accessor :restricted_signup_domains_raw
......
......@@ -53,6 +53,7 @@ module Ci
new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
new_build.save
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
end
end
......@@ -290,9 +291,15 @@ module Ci
end
def can_be_served?(runner)
return false unless has_tags? || runner.run_untagged?
(tag_list - runner.tag_list).empty?
end
def has_tags?
tag_list.any?
end
def any_runners_online?
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
......
......@@ -8,8 +8,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
delegate :stages, to: :statuses
validates_presence_of :sha
validates_presence_of :status
validate :valid_commit_sha
......@@ -22,7 +20,8 @@ module Ci
end
def self.stages
CommitStatus.where(commit: all).stages
# We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(commit: pluck(:id)).stages
end
def project_id
......@@ -67,6 +66,25 @@ module Ci
end
end
def cancel_running
builds.running_or_pending.each(&:cancel)
end
def retry_failed
builds.latest.failed.select(&:retryable?).each(&:retry)
end
def latest?
return false unless ref
commit = project.commit(ref)
return false unless commit
commit.sha == sha
end
def triggered?
trigger_requests.any?
end
def create_builds(user, trigger_request = nil)
return unless config_processor
config_processor.stages.any? do |stage|
......
......@@ -4,7 +4,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active]
FORM_EDITABLE = %i[description tag_list active run_untagged]
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
......@@ -26,6 +26,8 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
validate :tag_constraints
acts_as_taggable
# Searches for runners matching the given query.
......@@ -96,5 +98,18 @@ module Ci
def short_sha
token[0...8] if token
end
def has_tags?
tag_list.any?
end
private
def tag_constraints
unless has_tags? || run_untagged?
errors.add(:tags_list,
'can not be empty when runner is not allowed to pick untagged jobs')
end
end
end
end
......@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do
......@@ -45,6 +46,10 @@ class CommitStatus < ActiveRecord::Base
after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status)
end
end
delegate :sha, :short_sha, to: :commit
......@@ -54,13 +59,15 @@ class CommitStatus < ActiveRecord::Base
end
def self.stages
order_by = 'max(stage_idx)'
group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact
# We group by stage name, but order stages by theirs' index
unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end
def self.stages_status
all.stages.inject({}) do |h, stage|
h[stage] = all.where(stage: stage).status
# We execute subquery for each stage to calculate a stage status
statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
statuses.inject({}) do |h, k|
h[k.first] = k.last
h
end
end
......
......@@ -286,6 +286,18 @@ class MergeRequest < ActiveRecord::Base
last_commit == source_project.commit(source_branch)
end
def should_remove_source_branch?
merge_params['should_remove_source_branch'].present?
end
def force_remove_source_branch?
merge_params['force_remove_source_branch'].present?
end
def remove_source_branch?
should_remove_source_branch? || force_remove_source_branch?
end
def mr_and_commit_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
......@@ -426,7 +438,10 @@ class MergeRequest < ActiveRecord::Base
self.merge_when_build_succeeds = false
self.merge_user = nil
self.merge_params = nil
if merge_params
merge_params.delete('should_remove_source_branch')
merge_params.delete('commit_message')
end
self.save
end
......
......@@ -98,9 +98,7 @@ class MergeRequestDiff < ActiveRecord::Base
commits = compare.commits
if commits.present?
commits = Commit.decorate(commits, merge_request.source_project).
sort_by(&:created_at).
reverse
commits = Commit.decorate(commits, merge_request.source_project).reverse
end
commits
......
......@@ -59,8 +59,27 @@ class Milestone < ActiveRecord::Base
end
end
def self.reference_prefix
'%'
end
def self.reference_pattern
nil
# NOTE: The iid pattern only matches when all characters on the expression
# are digits, so it will match %2 but not %2.1 because that's probably a
# milestone name and we want it to be matched as such.
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
(?<milestone_iid>
\d+(?!\S\w)\b # Integer-based milestone iid, or
) |
(?<milestone_name>
[^"\s]+\b | # String-based single-word milestone title, or
"[^"]+" # String-based multi-word milestone surrounded in quotes
)
)
}x
end
def self.link_reference_pattern
......@@ -81,13 +100,26 @@ class Milestone < ActiveRecord::Base
end
end
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
h = Gitlab::Routing.url_helpers
url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
##
# Returns the String necessary to reference this Milestone in Markdown
#
# format - Symbol format to use (default: :iid, optional: :name)
#
# Examples:
#
# Milestone.first.to_reference # => "%1"
# Milestone.first.to_reference(format: :name) # => "%\"goal\""
# Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
"[#{escaped_title}](#{url})"
if cross_project_reference?(from_project)
project.to_reference + reference
else
reference
end
end
def reference_link_text(from_project = nil)
......@@ -159,4 +191,16 @@ class Milestone < ActiveRecord::Base
issues.where(id: ids).
update_all(["position = CASE #{conditions} ELSE position END", *pairs])
end
private
def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
if format == :name && !name.include?('"')
%("#{name}")
else
iid
end
end
end
......@@ -253,7 +253,7 @@ module Network
leaves = []
leaves.push(commit) if commit.space.zero?
while true
loop do
return leaves if commit.parents(@map).count.zero?
commit = commit.parents(@map).first
......
......@@ -171,17 +171,17 @@ class Project < ActiveRecord::Base
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) }
scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped }
scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
state_machine :import_status, initial: :none do
event :import_start do
......@@ -204,22 +204,10 @@ class Project < ActiveRecord::Base
state :finished
state :failed
after_transition any => :finished, do: :clear_import_data
after_transition any => :finished, do: :reset_cache_and_import_attrs
end
class << self
def abandoned
where('projects.last_activity_at < ?', 6.months.ago)
end
def with_push
joins(:events).where('events.action = ?', Event::PUSHED)
end
def active
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
......@@ -281,10 +269,6 @@ class Project < ActiveRecord::Base
projects.iwhere('projects.path' => project_path).take
end
def find_by_ci_id(id)
find_by(ci_id: id.to_i)
end
def visibility_levels
Gitlab::VisibilityLevel.options
end
......@@ -315,10 +299,6 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC')
end
def visible_to_user(user)
where(id: user.authorized_projects.select(:id).reorder(nil))
end
end
def team
......@@ -380,7 +360,7 @@ class Project < ActiveRecord::Base
end
end
def clear_import_data
def reset_cache_and_import_attrs
update(import_error: nil)
ProjectCacheWorker.perform_async(self.id)
......@@ -389,14 +369,14 @@ class Project < ActiveRecord::Base
end
def import_url=(value)
import_url = Gitlab::ImportUrl.new(value)
import_url = Gitlab::UrlSanitizer.new(value)
create_or_update_import_data(credentials: import_url.credentials)
super(import_url.sanitized_url)
end
def import_url
if import_data && super
import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials)
import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
import_url.full_url
else
super
......@@ -446,12 +426,7 @@ class Project < ActiveRecord::Base
end
def safe_import_url
result = URI.parse(self.import_url)
result.password = '*****' unless result.password.nil?
result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user
result.to_s
rescue
self.import_url
Gitlab::UrlSanitizer.new(import_url).masked_url
end
def check_limit
......
......@@ -35,8 +35,8 @@ class SlackService
private
def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)"
end
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
......
......@@ -245,7 +245,7 @@ class Repository
def cache_keys
%i(size branch_names tag_names commit_count
readme version contribution_guide changelog
license_blob license_key)
license_blob license_key gitignore)
end
def build_cache
......@@ -256,6 +256,10 @@ class Repository
end
end
def expire_gitignore
cache.expire(:gitignore)
end
def expire_tags_cache
cache.expire(:tag_names)
@tags = nil
......@@ -472,33 +476,37 @@ class Repository
def changelog
cache.fetch(:changelog) do
tree(:head).blobs.find do |file|
file.name =~ /\A(changelog|history|changes|news)/i
end
file_on_head(/\A(changelog|history|changes|news)/i)
end
end
def license_blob
return nil if !exists? || empty?
return nil unless head_exists?
cache.fetch(:license_blob) do
tree(:head).blobs.find do |file|
file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i
end
file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
end
end
def license_key
return nil if !exists? || empty?
return nil unless head_exists?
cache.fetch(:license_key) do
Licensee.license(path).try(:key)
end
end
def gitlab_ci_yml
def gitignore
return nil if !exists? || empty?
cache.fetch(:gitignore) do
file_on_head(/\A\.gitignore\z/)
end
end
def gitlab_ci_yml
return nil unless head_exists?
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
......@@ -854,7 +862,7 @@ class Repository
def search_files(query, ref)
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref})
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
......@@ -965,7 +973,7 @@ class Repository
end
def main_language
return if empty? || rugged.head_unborn?
return unless head_exists?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end
......@@ -985,4 +993,12 @@ class Repository
def cache
@cache ||= RepositoryCache.new(path_with_namespace)
end
def head_exists?
exists? && !empty? && !rugged.head_unborn?
end
def file_on_head(regex)
tree(:head).blobs.find { |file| file.name =~ regex }
end
end
class Todo < ActiveRecord::Base
ASSIGNED = 1
MENTIONED = 2
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
belongs_to :author, class_name: "User"
belongs_to :note
......@@ -28,6 +29,10 @@ class Todo < ActiveRecord::Base
state :done
end
def build_failed?
action == BUILD_FAILED
end
def body
if note.present?
note.note
......
......@@ -398,11 +398,6 @@ class User < ActiveRecord::Base
owned_groups.select(:id), namespace.id).joins(:namespace)
end
# Team membership in authorized projects
def tm_in_authorized_projects
ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id)
end
def is_admin?
admin
end
......@@ -492,10 +487,6 @@ class User < ActiveRecord::Base
"#{name} (#{username})"
end
def tm_of(project)
project.project_member_by_id(self.id)
end
def already_forked?(project)
!!fork_of(project)
end
......
......@@ -6,7 +6,7 @@ module Auth
return error('not found', 404) unless registry.enabled
if params[:offline_token]
return error('unauthorized', 401) unless current_user
return error('unauthorized', 401) unless current_user || project
else
return error('forbidden', 403) unless scope
end
......@@ -20,7 +20,7 @@ module Auth
token.issuer = registry.issuer
token.audience = AUDIENCE
token[:access] = names.map do |name|
{ type: 'repository', name: name, actions: %w(pull push) }
{ type: 'repository', name: name, actions: %w(*) }
end
token.encoded
end
......
module Ci
class CreatePipelineService < BaseService
def execute
pipeline = project.ci_commits.new(params)
unless ref_names.include?(params[:ref])
pipeline.errors.add(:base, 'Reference not found')
return pipeline
end
unless commit
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
unless can?(current_user, :create_pipeline, project)
pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
return pipeline
end
begin
Ci::Commit.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
raise ActiveRecord::Rollback
end
pipeline.save!
pipeline.create_builds(current_user)
end
rescue
pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
end
pipeline
end
private
def ref_names
@ref_names ||= project.repository.ref_names
end
def commit
@commit ||= project.commit(params[:ref])
end
end
end
......@@ -18,19 +18,16 @@ class CreateCommitBuildsService
return false
end
commit = project.ci_commit(sha, ref)
unless commit
commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag)
commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file
return false
end
# Create a new ci_commit
commit.save!
# Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file
return false
end
# Create a new ci_commit
commit.save!
# Skip creating builds for commits that have [ci skip]
unless commit.skip_ci?
# Create builds for commit
......
......@@ -24,6 +24,10 @@ module Issues
todo_service.reassigned_issue(issue, current_user)
end
if issue.previous_changes.include?('confidential')
create_confidentiality_note(issue)
end
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
......@@ -37,5 +41,11 @@ module Issues
def close_service
Issues::CloseService
end
private
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
end
end
end
module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status)
each_merge_request(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
# Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status)
each_merge_request(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
end
end
......@@ -38,5 +38,30 @@ module MergeRequests
def filter_params
super(:merge_request)
end
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
def each_merge_request(commit_status)
merge_request_from(commit_status).each do |merge_request|
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
yield merge_request, ci_commit
end
end
end
end
......@@ -8,11 +8,14 @@ module MergeRequests
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
filter_params
label_params = params[:label_ids]
merge_request = MergeRequest.new(params.except(:label_ids))
label_params = params.delete(:label_ids)
force_remove_source_branch = params.delete(:force_remove_source_branch)
merge_request = MergeRequest.new(params)
merge_request.source_project = source_project
merge_request.target_project ||= source_project
merge_request.author = current_user
merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
if merge_request.save
merge_request.update_attributes(label_ids: label_params)
......
......@@ -45,10 +45,14 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch].present?
DeleteBranchService.new(@merge_request.source_project, current_user).
if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user).
execute(merge_request.source_branch)
end
end
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
end
end
......@@ -20,15 +20,9 @@ module MergeRequests
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(commit_status)
merge_requests = merge_request_from(commit_status)
merge_requests.each do |merge_request|
each_merge_request(commit_status) do |merge_request, ci_commit|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
next unless ci_commit.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
......@@ -47,20 +41,5 @@ module MergeRequests
end
end
private
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
end
end
......@@ -12,6 +12,7 @@ module MergeRequests
close_merge_requests
reload_merge_requests
reset_merge_when_build_succeeds
mark_pending_todos_done
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
......@@ -80,6 +81,12 @@ module MergeRequests
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
end
def mark_pending_todos_done
merge_requests_for_source_branch.each do |merge_request|
todo_service.merge_request_push(merge_request, @current_user)
end
end
def find_new_commits
if branch_added?
@commits = []
......
......@@ -11,6 +11,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
update(merge_request)
end
......
......@@ -4,6 +4,10 @@ module Projects
@project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
def milestones
@project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title])
end
def merge_requests
@project.merge_requests.opened.select([:iid, :title])
end
......
......@@ -169,12 +169,33 @@ class SystemNoteService
#
# Returns the created Note object
def self.change_title(noteable, project, author, old_title)
return unless noteable.respond_to?(:title)
new_title = noteable.title.dup
body = "Title changed from **#{old_title}** to **#{noteable.title}**"
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the confidentiality changes
#
# issue - Issue object
# project - Project owning the issue
# author - User performing the change
#
# Example Note text:
#
# "Made the issue confidential"
#
# Returns the created Note object
def self.change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible'
create_note(noteable: issue, project: project, author: author, note: body)
end
# Called when a branch in Noteable is changed
#
# noteable - Noteable object
......
......@@ -80,6 +80,30 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
# When a build fails on the HEAD of a merge request we should:
#
# * create a todo for that user to fix it
#
def merge_request_build_failed(merge_request)
create_build_failed_todo(merge_request)
end
# When a new commit is pushed to a merge request we should:
#
# * mark all pending todos related to the merge request for that user as done
#
def merge_request_push(merge_request, current_user)
mark_pending_todos_as_done(merge_request, current_user)
end
# When a build is retried to a merge request we should:
#
# * mark all pending todos related to the merge request for the author as done
#
def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author)
end
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
......@@ -153,6 +177,12 @@ class TodoService
create_todos(mentioned_users, attributes)
end
def create_build_failed_todo(merge_request)
author = merge_request.author
attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
create_todos(author, attributes)
end
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
......
......@@ -9,8 +9,6 @@
%span.runner-state.runner-state-specific
Specific
- if @runner.shared?
.bs-callout.bs-callout-success
%h4 This runner will process builds from ALL UNASSIGNED projects
......
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title.title
%span.author-name
- if todo.author
= link_to_author(todo)
- else
(removed)
- unless todo.build_failed?
%span.author-name
- if todo.author
= link_to_author(todo)
- else
(removed)
%span.todo-label
= todo_action_name(todo)
- if todo.target
......
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do
= link_to dashboard_projects_path, title: 'Projects' do
= 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('bookmark fw')
%span
Projects
......@@ -11,7 +11,7 @@
Todos
%span.count.todos-pending-count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
%span
Activity
......@@ -26,13 +26,13 @@
%span
Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
= icon('exclamation-circle fw')
%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: 'shortcuts-merge_requests' do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
= icon('tasks fw')
%span
Merge Requests
......
%ul.nav.nav-sidebar
- if @project.group
= nav_link do
= link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to group
- else
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
- if current_user
.controls
- access = user_max_access_in_project(current_user.id, @project)
- can_edit = can?(current_user, :admin_project, @project)
.dropdown.project-settings-dropdown
%a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
= render 'layouts/nav/project_settings'
%li.divider
- if can_edit
%li
= link_to edit_project_path(@project) do
Edit Project
- if access
%li
= link_to leave_namespace_project_project_members_path(@project.namespace, @project),
data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
%ul.nav-links
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('bookmark fw')
......@@ -38,13 +44,21 @@
%span
Commits
- if project_nav_tab? :pipelines
= nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
= icon('ship fw')
%span
Pipelines
%span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count)
- if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
= icon('cubes fw')
%span
Builds
%span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
%span.badge.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :container_registry
= nav_link(controller: %w(container_registry)) do
......@@ -74,7 +88,7 @@
%span
Issues
- if @project.default_issues_tracker?
%span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
%span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
......@@ -82,14 +96,7 @@
= icon('tasks fw')
%span
Merge Requests
%span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :team
= nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
= icon('users fw')
%span
Members
%span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :labels
= nav_link(controller: :labels) do
......@@ -105,13 +112,6 @@
%span
Wiki
- if project_nav_tab? :forks
= nav_link(controller: :forks, action: :index) do
= link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do
= icon('code-fork fw')
%span
Forks
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do
......@@ -119,13 +119,6 @@
%span
Snippets
- if project_nav_tab? :settings
= nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
= link_to edit_project_path(@project), title: 'Settings' do
= icon('cogs fw')
%span
Settings
-# Global shortcut to network page for compatibility
- if project_nav_tab? :network
%li.hidden
......
%ul.nav.nav-sidebar
= nav_link do
= link_to project_path(@project), title: 'Go to project', class: 'back-link' do
= icon('caret-square-o-left fw')
- if project_nav_tab? :team
= nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Go to project
Members
%li.separate-item
%ul.sidebar-subnav
= nav_link(path: 'projects#edit') do
= link_to edit_project_path(@project), title: 'Project Settings' do
= icon('pencil-square-o fw')
%span
Project Settings
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
= icon('share-square-o fw')
%span
Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link(controller: :hooks) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
= icon('link fw')
%span
Webhooks
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
= icon('cogs fw')
%span
Services
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
= icon('lock fw')
%span
Protected Branches
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
%span
Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
Deploy Keys
= nav_link(controller: :hooks) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
%span
Webhooks
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
%span
Services
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span
Protected Branches
- if @project.builds_enabled?
= nav_link(controller: :runners) do
= link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
= icon('cog fw')
%span
Runners
= nav_link(controller: :variables) do
= link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
= icon('code fw')
%span
Variables
= nav_link(controller: :triggers) do
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
= icon('retweet fw')
%span
Triggers
= nav_link(controller: :badges) do
= link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
= icon('star-half-empty fw')
%span
Badges
- if @project.builds_enabled?
= nav_link(controller: :runners) do
= link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
%span
Runners
= nav_link(controller: :variables) do
= link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
%span
Variables
= nav_link(controller: :triggers) do
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
%span
Triggers
= nav_link(controller: :badges) do
= link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
%span
Badges
......@@ -4,6 +4,7 @@
%title
GitLab
= stylesheet_link_tag 'notify'
= yield :head
%body
%div.content
= yield
......
- page_title @project.name_with_namespace
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
- sidebar "project" unless sidebar
- nav "project"
- content_for :scripts_body_top do
- project = @target_project || @project
......
- page_title "Settings"
- header_title project_title(@project, "Settings", edit_project_path(@project))
- sidebar "project_settings"
- nav "project"
= render template: "layouts/project"
= content_for :head do
= stylesheet_link_tag 'mailers/repository_push_email'
%h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))}
......@@ -43,26 +46,38 @@
= diff.new_path
- unless @message.disable_diffs?
%h4 Changes:
- @message.diffs.each_with_index do |diff, i|
%li{id: "diff-#{i}"}
%a{href: @message.target_url + "#diff-#{i}"}
- if diff.deleted_file
%strong
= diff.old_path
deleted
- elsif diff.renamed_file
%strong
= diff.old_path
&rarr;
%strong
= diff.new_path
- else
%strong
= diff.new_path
%hr
= color_email_diff(diff.diff)
%br
- diff_files = @message.diffs
- if @message.compare_timeout
%h5 Huge diff. To prevent performance issues changes are hidden
- if @message.compare_timeout
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- diff_files.each_with_index do |diff_file, i|
%li{id: "diff-#{i}"}
%a{href: @message.target_url + "#diff-#{i}"}<
- if diff_file.deleted_file
%strong<
= diff_file.old_path
deleted
- elsif diff_file.renamed_file
%strong<
= diff_file.old_path
&rarr;
%strong<
= diff_file.new_path
- else
%strong<
= diff_file.new_path
- if diff_file.too_large?
The diff for this file was not included because it is too large.
- else
%hr
- diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last
- blob = @message.project.repository.blob_for_diff(diff_commit, diff_file)
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- diff_file.highlighted_diff_lines.each do |line|
= render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true}
- else
No preview for this file type
%br
......@@ -25,24 +25,28 @@
- else
\- #{diff.new_path}
- unless @message.disable_diffs?
\
\
Changes:
- @message.diffs.each do |diff|
- if @message.compare_timeout
\
\=====================================
- if diff.deleted_file
#{diff.old_path} deleted
- elsif diff.renamed_file
#{diff.old_path}#{diff.new_path}
- else
= diff.new_path
\=====================================
!= diff.diff
- if @message.compare_timeout
\
\
Huge diff. To prevent performance issues it was hidden
\
The diff was not included because it is too large.
- else
\
\
Changes:
- @message.diffs.each do |diff_file|
\
\=====================================
- if diff_file.deleted_file
#{diff_file.old_path} deleted
- elsif diff_file.renamed_file
#{diff_file.old_path}#{diff_file.new_path}
- else
= diff_file.new_path
\=====================================
- if diff_file.too_large?
The diff for this file was not included because it is too large.
- else
!= diff_file.diff.diff
- if @message.target_url
\
\
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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