Commit 2846f95d authored by Grzegorz Bizon's avatar Grzegorz Bizon

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

* master: (352 commits)
  Display last commit of deleted branch in push events (!4699)
  add changelog
  add missing attribute to attr_encrypted so it is fully backwards-compatible
  Add "GitLab team members only" to diagram link
  doc: note that .gitattributes uses default branch
  use the conf lexer so we have highlighted comments
  first draft of docs
  support cgi style options, such as erb?parent=json
  move the path alias to a more appropriate location
  make #custom_language private
  appease rubocop
  add an alias for Snippet#path
  appease rubocop
  check the tag so that an instance will pass too
  fix the spec, using project.change_head
  Revert "bump the master sha for gitlab-test!9"
  bump the master sha for gitlab-test!9
  add custom highlighting via .gitattributes
  Rename Licenses API to License Templates API
  Check for conflict with wiki projects when creating a new project.
  ...
parents c019585c 365015e3
......@@ -134,6 +134,11 @@ spinach 9 10: *spinach-knapsack
image: "ruby:2.3"
only:
- master
cache:
key: "ruby-23"
paths:
- vendor/apt
- vendor/ruby
.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
<<: *rspec-knapsack
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
v 8.10.0 (unreleased)
- Replace Haml with Hamlit to make view rendering faster. !3666
- Wrap code blocks on Activies and Todos page. !4783 (winniehell)
- Display last commit of deleted branch in push events !4699 (winniehell)
- Add Sidekiq queue duration to transaction metrics.
- 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
- Fix pagination when sorting by columns with lots of ties (like priority)
- Exclude email check from the standard health check
- Fix changing issue state columns in milestone view
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- Check for conflicts with existing Project's wiki path when creating a new project.
- Add API endpoint for a group issues !4520 (mahcsig)
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
v 8.9.3 (unreleased)
- Fix encrypted data backwards compatibility after upgrading attr_encrypted gem
v 8.9.2
- Fix visibility of snippets when searching.
- Fix an information disclosure when requesting access to a group containing private projects.
- Update omniauth-saml to 1.6.0 !4951
v 8.9.1
- Refactor labels documentation. !3347
- Eager load award emoji on notes. !4628
- Fix some CI wording in documentation. !4660
- Document `GIT_STRATEGY` and `GIT_DEPTH`. !4720
- Add documentation for the export & import features. !4732
- Add some docs for Docker Registry configuration. !4738
- Ensure we don't send the "access request declined" email to access requesters on project deletion. !4744
- Display group/project access requesters separately in the admin area. !4798
- Add documentation and examples for configuring cloud storage for registry images. !4812
- Clarifies documentation about artifact expiry. !4831
- Fix the Network graph links. !4832
- Fix MR-auto-close text added to description. !4836
- Add documentation for award emoji now that comments can be awarded with emojis. !4839
- Fix typo in export failure email. !4847
- Fix header vertical centering. !4170
- Fix subsequent SAML sign ins. !4718
- Set button label when picking an option from status dropdown. !4771
- Prevent invalid URLs from raising exceptions in WikiLink Filter. !4775
- Handle external issues in IssueReferenceFilter. !4789
- Support for rendering/redacting multiple documents. !4828
- Update Todos documentation and screenshots to include new functionality. !4840
- Hide nav arrows by default. !4843
- Added bottom padding to label color suggestion link. !4845
- Use jQuery objects in ref dropdown. !4850
- Fix GitLab project import issues related to notes and builds. !4855
- Restrict header logo to 36px so it doesn't overflow. !4861
- Fix unwanted label unassignment. !4863
- Fix mobile Safari bug where horizontal nav arrows would flicker on scroll. !4869
- Restore old behavior around diff notes to outdated discussions. !4870
- Fix merge requests project settings help link anchor. !4873
- Fix 404 when accessing pipelines as guest user on public projects. !4881
- Remove width restriction for logo on sign-in page. !4888
- Bump gitlab_git to 10.2.3 to fix false truncated warnings with ISO-8559 files. !4884
- Apply selected value as label. !4886
- Fix temp file being deleted after the request while importing a GitLab project. !4894
- Fix pagination when sorting by columns with lots of ties (like priority)
- Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise.
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true. !4912
- Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826. !4915
- Remove duplicate 'New Page' button on edit wiki page
v 8.9.0
- Fix builds API response not including commit data
- Fix error when CI job variables key specified but not defined
- Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker
......@@ -8,17 +76,21 @@ v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Show Star and Fork buttons on mobile.
- Performance improvements on RelativeLinkFilter
- Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
- Pre-processing Markdown now only happens when needed
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Redesign all Devise emails. !4297
- Don't show 'Leave Project' to group members
- Fix wiki page events' webhook to point to the wiki repository
- Add a border around images to differentiate them from the background.
- Don't show tags for revert and cherry-pick operations
- Show image ID on registry page
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Allow customisable text on the 'nearly there' page after a user signs up
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
......@@ -55,6 +127,7 @@ v 8.9.0 (unreleased)
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state
- Limit email on push diff size to 30 files / 150 KB
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Fix race condition on merge when build succeeds
......@@ -69,6 +142,7 @@ v 8.9.0 (unreleased)
- Todos will display target state if issuable target is 'Closed' or 'Merged'
- Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
- POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
......@@ -76,13 +150,16 @@ v 8.9.0 (unreleased)
- Pipelines can be canceled only when there are running builds
- Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker
- Allow to use CI token to fetch LFS objects
- Custom notification settings
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
- Added Gfm autocomplete for labels
- Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114
- Make Omniauth providers specs to not modify global configuration
- Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11
- Make it possible to lock a runner from being enabled for other projects
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment
......@@ -100,6 +177,7 @@ v 8.9.0 (unreleased)
- An indicator is now displayed at the top of the comment field for confidential issues.
- Show categorised search queries in the search autocomplete
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Dropdown for `.gitlab-ci.yml` templates
- Improve issuables APIs performance when accessing notes !4471
- Add sorting dropdown to tags page !4423
- External links now open in a new tab
......@@ -128,10 +206,17 @@ v 8.9.0 (unreleased)
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
- Set inverse_of for Project/Service association to reduce the number of queries
- Update tanuki logo highlight/loading colors
- Remove explicit Gitlab::Metrics.action assignments, are already automatic.
- Use Git cached counters for branches and tags on project page
- Cache participable participants in an instance variable.
- Filter parameters for request_uri value on instrumented transactions.
- Remove duplicated keys add UNIQUE index to keys fingerprint column
- ExtractsPath get ref_names from repository cache, if not there access git.
- Cache user todo counts from TodoService
- Ensure Todos counters doesn't count Todos for projects pending delete
- Add left/right arrows horizontal navigation
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
v 8.8.5
- Import GitHub repositories respecting the API rate limit !4166
......
......@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-saml', '~> 1.6.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......@@ -48,7 +48,7 @@ gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# Browser detection
gem "browser", '~> 2.0.3'
gem "browser", '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
......@@ -76,7 +76,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem "kaminari", "~> 0.17.0"
# HAML
gem "haml-rails", '~> 0.9.0'
gem 'hamlit', '~> 2.5'
# Files attachments
gem "carrierwave", '~> 0.10.0'
......@@ -234,7 +234,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 0.15'
gem 'sentry-raven', '~> 1.1.0'
gem 'premailer-rails', '~> 1.9.0'
......@@ -330,7 +330,7 @@ gem "newrelic_rpm", '~> 3.14'
gem 'octokit', '~> 4.3.0'
gem "mail_room", "~> 0.7"
gem "mail_room", "~> 0.8"
gem 'email_reply_parser', '~> 0.5.8'
......
......@@ -98,7 +98,7 @@ GEM
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.3.2)
browser (2.0.3)
browser (2.2.0)
builder (3.2.2)
bullet (5.0.0)
activesupport (>= 3.0.0)
......@@ -277,7 +277,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.2.0)
gitlab_git (10.2.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -320,14 +320,10 @@ GEM
grape-entity (0.4.8)
activesupport
multi_json (>= 1.3.2)
haml (4.0.7)
hamlit (2.5.0)
temple (~> 0.7.6)
thor
tilt
haml-rails (0.9.0)
actionpack (>= 4.0.1)
activesupport (>= 4.0.1)
haml (>= 4.0.6, < 5.0)
html2haml (>= 1.0.1)
railties (>= 4.0.1)
hashie (3.4.3)
health_check (1.5.1)
rails (>= 2.3.0)
......@@ -337,11 +333,6 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
html2haml (2.0.0)
erubis (~> 2.7.0)
haml (~> 4.0.0)
nokogiri (~> 1.6.0)
ruby_parser (~> 3.5)
htmlentities (4.3.4)
http_parser.rb (0.5.3)
httparty (0.13.7)
......@@ -398,7 +389,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.7.0)
mail_room (0.8.0)
method_source (0.8.2)
mime-types (2.99.2)
mimemagic (0.3.0)
......@@ -468,9 +459,9 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-saml (1.5.0)
omniauth-saml (1.6.0)
omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1)
ruby-saml (~> 1.3)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
......@@ -631,9 +622,8 @@ GEM
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.8.1)
ruby-saml (1.1.2)
ruby-saml (1.3.0)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby_parser (3.8.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
......@@ -665,7 +655,7 @@ GEM
activesupport (>= 3.1, < 4.3)
select2-rails (3.5.9.3)
thor (~> 0.14)
sentry-raven (0.15.6)
sentry-raven (1.1.0)
faraday (>= 0.7.6)
settingslogic (2.0.9)
sexp_processor (4.7.0)
......@@ -733,6 +723,7 @@ GEM
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.7.7)
term-ansicolor (1.3.2)
tins (~> 1.0)
test_after_commit (0.4.2)
......@@ -833,7 +824,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0)
browser (~> 2.0.3)
browser (~> 2.2)
bullet
bundler-audit
byebug
......@@ -882,7 +873,7 @@ DEPENDENCIES
gon (~> 6.0.1)
grape (~> 0.13.0)
grape-entity (~> 0.4.2)
haml-rails (~> 0.9.0)
hamlit (~> 2.5)
health_check (~> 1.5.1)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
......@@ -899,7 +890,7 @@ DEPENDENCIES
license_finder
licensee (~> 8.0.0)
loofah (~> 2.0.3)
mail_room (~> 0.7)
mail_room (~> 0.8)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
......@@ -920,7 +911,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.5.0)
omniauth-saml (~> 1.6.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
......@@ -960,7 +951,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
sentry-raven (~> 0.15)
sentry-raven (~> 1.1.0)
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
......
8.9.0-pre
8.10.0-pre
......@@ -7,6 +7,7 @@
labelsPath: "/api/:version/projects/:id/labels"
licensePath: "/api/:version/licenses/:key"
gitignorePath: "/api/:version/gitignores/:key"
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key"
group: (group_id, callback) ->
url = Api.buildUrl(Api.groupPath)
......@@ -110,6 +111,12 @@
$.get url, (gitignore) ->
callback(gitignore)
gitlabCiYml: (key, callback) ->
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key)
$.get url, (file) ->
callback(file)
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
......@@ -50,7 +50,7 @@
#= require_directory ./ci
#= require_directory ./commit
#= require_directory ./extensions
#= require_directory ./lib
#= require_directory ./lib/utils
#= require_directory ./u2f
#= require_directory .
#= require fuzzaldrin-plus
......@@ -121,6 +121,11 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
$document = $(document)
$window = $(window)
$body = $('body')
gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize()
......@@ -152,7 +157,7 @@ $ ->
), 1
# Initialize tooltips
$('body').tooltip(
$body.tooltip(
selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
......@@ -171,7 +176,7 @@ $ ->
flash.show()
# Disable form buttons while a form is submitting
$('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
$body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
buttons = $('[type="submit"]', @)
switch e.type
......@@ -184,7 +189,7 @@ $ ->
$('.account-box').hover -> $(@).toggleClass('hover')
# Commit show suppressed diff
$(document).on 'click', '.diff-content .js-show-suppressed-diff', ->
$document.on 'click', '.diff-content .js-show-suppressed-diff', ->
$container = $(@).parent()
$container.next('table').show()
$container.remove()
......@@ -197,13 +202,13 @@ $ ->
$('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
# Show/hide comments on diff
$("body").on "click", ".js-toggle-diff-comments", (e) ->
$body.on "click", ".js-toggle-diff-comments", (e) ->
$(@).toggleClass('active')
$(@).closest(".diff-file").find(".notes_holder").toggle()
e.preventDefault()
$(document).off "click", '.js-confirm-danger'
$(document).on "click", '.js-confirm-danger', (e) ->
$document.off "click", '.js-confirm-danger'
$document.on "click", '.js-confirm-danger', (e) ->
e.preventDefault()
btn = $(e.target)
text = btn.data("confirm-danger-message")
......@@ -211,7 +216,7 @@ $ ->
new ConfirmDangerModal(form, text)
$(document).on 'click', 'button', ->
$document.on 'click', 'button', ->
$(this).blur()
$('input[type="search"]').each ->
......@@ -219,7 +224,7 @@ $ ->
$this.attr 'value', $this.val()
return
$(document)
$document
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
$this = $(this)
......@@ -227,7 +232,7 @@ $ ->
$sidebarGutterToggle = $('.js-sidebar-toggle')
$(document)
$document
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
......@@ -239,14 +244,14 @@ $ ->
oldBootstrapBreakpoint = bootstrapBreakpoint
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
$document.trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
$document.trigger('breakpoint:change', [bootstrapBreakpoint])
$(window)
$window
.off "resize.app"
.on "resize.app", (e) ->
fitSidebarForSize()
......@@ -256,29 +261,45 @@ $ ->
new Aside()
# Sidenav pinning
if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
$.cookie('pin_nav', 'false')
if $window.width() < 1440 and $.cookie('pin_nav') is 'true'
$.cookie('pin_nav', 'false', { path: '/' })
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
.removeClass('page-sidebar-pinned')
$('.navbar-fixed-top').removeClass('header-pinned-nav')
$(document)
$document
.off 'click', '.js-nav-pin'
.on 'click', '.js-nav-pin', (e) ->
e.preventDefault()
$pinBtn = $(e.currentTarget)
$page = $ '.page-with-sidebar'
$topNav = $ '.navbar-fixed-top'
$tooltip = $ "##{$pinBtn.attr('aria-describedby')}"
doPinNav = not $page.is('.page-sidebar-pinned')
tooltipText = 'Pin navigation'
$(this).toggleClass 'is-active'
if $.cookie('pin_nav') is 'true'
$.cookie 'pin_nav', 'false'
$('.page-with-sidebar')
.removeClass('page-sidebar-pinned')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.removeClass('header-pinned-nav')
.toggleClass('header-collapsed header-expanded')
if doPinNav
$page.addClass('page-sidebar-pinned')
$topNav.addClass('header-pinned-nav')
else
$.cookie 'pin_nav', 'true'
$('.page-with-sidebar').addClass('page-sidebar-pinned')
$('.navbar-fixed-top').addClass('header-pinned-nav')
$tooltip.remove() # Remove it immediately when collapsing the sidebar
$page.removeClass('page-sidebar-pinned')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$topNav.removeClass('header-pinned-nav')
.toggleClass('header-collapsed header-expanded')
# Save settings
$.cookie 'pin_nav', doPinNav, { path: '/' }
if $.cookie('pin_nav') is 'true' or doPinNav
tooltipText = 'Unpin navigation'
# Update tooltip text immediately
$tooltip.find('.tooltip-inner').text(tooltipText)
# Persist tooltip title
$pinBtn.attr('title', tooltipText).tooltip('fixTitle')
......@@ -341,7 +341,9 @@ class @AwardsHandler
for emoji in frequentlyUsedEmojis
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
$('.emoji-menu-content')
.prepend(ul)
.prepend($('<h5>').text('Frequently used'))
@frequentEmojiBlockRendered = true
......@@ -356,7 +358,7 @@ class @AwardsHandler
if term
# Generate a search result block
h5 = $('<h5>').text('Search results').addClass('emoji-search')
h5 = $('<h5>').text('Search results')
found_emojis = @searchEmojis(term).show()
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
$('.emoji-menu-content ul, .emoji-menu-content h5').hide()
......
#= require blob/template_selector
class @BlobCiYamlSelector extends TemplateSelector
requestFile: (query) ->
Api.gitlabCiYml query.name, @requestFileSuccess.bind(@)
class @BlobCiYamlSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitlab-ci-yml-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobCiYamlSelector(
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
......@@ -15,6 +15,7 @@ class @EditBlob
new BlobLicenseSelectors { @editor }
new BlobGitignoreSelectors { @editor }
new BlobCiYamlSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
......
......@@ -19,6 +19,7 @@ class @TemplateSelector
data: @data,
filterable: true,
selectable: true,
toggleLabel: @toggleLabel,
search:
fields: ['name']
clicked: @onClick
......@@ -31,6 +32,9 @@ class @TemplateSelector
@onFilenameUpdate()
)
toggleLabel: (item) ->
item.name
onFilenameUpdate: ->
return unless @$input.length
......
......@@ -58,7 +58,7 @@ class GitLabDropdownFilter
filter: (search_text) ->
data = @options.data()
if data?
if data? and not @options.filterByText
results = data
if search_text isnt ''
......@@ -102,10 +102,11 @@ class GitLabDropdownFilter
$el = $(@)
matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
if matches.length
$el.show()
else
$el.hide()
unless $el.is('.dropdown-header')
if matches.length
$el.show()
else
$el.hide()
else
elements.show()
......@@ -191,6 +192,7 @@ class GitLabDropdown
if @options.filterable
@filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur
filterByText: @options.filterByText
remote: @options.filterRemote
query: @options.data
keys: searchFields
......@@ -278,7 +280,7 @@ class GitLabDropdown
html = @renderData(data)
# Render the full menu
full_html = @renderMenu(html.join(""))
full_html = @renderMenu(html)
@appendMenu(full_html)
......@@ -349,7 +351,8 @@ class GitLabDropdown
if @options.renderMenu
menu_html = @options.renderMenu(html)
else
menu_html = "<ul>#{html}</ul>"
menu_html = $('<ul />')
.append(html)
return menu_html
......@@ -358,7 +361,9 @@ class GitLabDropdown
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
$(selector, @dropdown).html html
$(selector, @dropdown)
.empty()
.append(html)
# Render the row
renderItem: (data, group = false, index = false) ->
......@@ -457,7 +462,7 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
@updateLabel()
@updateLabel(selectedObject, el, @)
else
selectedObject
else if el.hasClass(INDETERMINATE_CLASS)
......@@ -484,7 +489,7 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
@updateLabel(selectedObject, el)
@updateLabel(selectedObject, el, @)
if value?
if !field.length and fieldName
@addInput(fieldName, value)
......@@ -583,8 +588,8 @@ class GitLabDropdown
# Scroll the dropdown content up
$dropdownContent.scrollTop(listItemTop - dropdownContentTop)
updateLabel: (selected = null, el = null) =>
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el)
updateLabel: (selected = null, el = null, instance = null) =>
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el, instance)
$.fn.glDropdown = (opts) ->
return @.each ->
......
......@@ -34,6 +34,8 @@ class @GLForm
# form and textarea event listeners
@addEventListeners()
gl.text.init(@form)
# hide discard button
@form.find('.js-note-discard').hide()
......@@ -42,6 +44,7 @@ class @GLForm
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
gl.text.removeListeners(@form)
addEventListeners: ->
@textarea.on 'focus', ->
......
......@@ -4,5 +4,4 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require Chart
#= require_tree .
......@@ -121,7 +121,11 @@ class @ContributorsMasterGraph extends ContributorsGraph
class @ContributorsAuthorGraph extends ContributorsGraph
constructor: (@data) ->
@width = $('.content').width()/2 - 100
# Don't split graph size in half for mobile devices.
if $(window).width() < 768
@width = $('.content').width() - 80
else
@width = ($('.content').width() / 2) - 100
@height = 200
@x = null
@y = null
......
......@@ -68,12 +68,15 @@ issuable_created = false
Turbolinks.visit(issuesUrl);
initChecks: ->
@issuableBulkActions = $('.bulk-update').data('bulkActions')
$('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked)
Issuable.checkChanged()
)
$('.selected_issue').off('change').on('change', Issuable.checkChanged)
$('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(@))
checkChanged: ->
checked_issues = $('.selected_issue:checked')
......@@ -88,3 +91,6 @@ issuable_created = false
$('#update_issues_ids').val []
$('.issues_bulk_update').hide()
$('.issues-other-filters').show()
@issuableBulkActions.willUpdateLabels = false
return true
......@@ -99,7 +99,7 @@ class @Issue
# If the user doesn't have the required permissions the container isn't
# rendered at all.
return unless $container
return if $container.length is 0
$.getJSON($container.data('path'))
.error ->
......
......@@ -6,6 +6,13 @@ class @IssueStatusSelect
$(el).glDropdown(
selectable: true
fieldName: fieldName
toggleLabel: (selected, el, instance) =>
label = 'Author'
$item = instance.dropdown.find('.is-active')
label = $item.text() if $item.length
label
clicked: (item, $el, e)->
e.preventDefault()
id: (obj, el) ->
$(el).data("id")
)
......@@ -7,6 +7,11 @@ class @IssuableBulkActions
@issues = @getElement('.issues-list .issue')
} = opts
# Save instance
@form.data 'bulkActions', @
@willUpdateLabels = false
@bindEvents()
# Fixes bulk-assign not working when navigating through pages
......@@ -87,11 +92,12 @@ class @IssuableBulkActions
add_label_ids : []
remove_label_ids : []
@getLabelsToApply().map (id) ->
formData.update.add_label_ids.push id
if @willUpdateLabels
@getLabelsToApply().map (id) ->
formData.update.add_label_ids.push id
@getLabelsToRemove().map (id) ->
formData.update.remove_label_ids.push id
@getLabelsToRemove().map (id) ->
formData.update.remove_label_ids.push id
formData
......
......@@ -319,6 +319,8 @@ class @LabelsSelect
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
_this.enableBulkLabelDropdown()
if $dropdown.hasClass('js-filter-bulk-update')
return
......@@ -377,3 +379,8 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels')
_.intersection.apply _, label_ids
enableBulkLabelDropdown: ->
if $('.selected_issue:checked').length
issuableBulkActions = $('.bulk-update').data('bulkActions')
issuableBulkActions.willUpdateLabels = true
......@@ -3,11 +3,10 @@ hideEndFade = ($scrollingTabs) ->
$this = $(@)
$this
.find('.fade-right')
.toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
.siblings('.fade-right')
.toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'))
$ ->
$('.fade-left').addClass('end-scroll')
hideEndFade($('.scrolling-tabs'))
......@@ -21,5 +20,5 @@ $ ->
currentPosition = $this.scrollLeft()
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
$this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0)
$this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1)
#= require raphael
#= require g.raphael
#= require g.bar
((w) ->
w.gl ?= {}
w.gl.text ?= {}
gl.text.randomString = -> Math.random().toString(36).substring(7)
gl.text.replaceRange = (s, start, end, substitute) ->
s.substring(0, start) + substitute + s.substring(end);
gl.text.selectedText = (text, textarea) ->
text.substring(textarea.selectionStart, textarea.selectionEnd)
gl.text.insertText = (textArea, text, tag, selected, wrap) ->
selectedSplit = selected.split('\n')
startChar = if not wrap and textArea.selectionStart > 0 then '\n' else ''
if selectedSplit.length > 1 and not wrap
insertText = selectedSplit.map((val) ->
if val.indexOf(tag) is 0
"#{val.replace(tag, '')}"
else
"#{tag}#{val}"
).join('\n')
else
insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}"
if document.queryCommandSupported('insertText')
document.execCommand 'insertText', false, insertText
else
try
document.execCommand("ms-beginUndoUnit")
textArea.value = @replaceRange(
text,
textArea.selectionStart,
textArea.selectionEnd,
insertText)
try
document.execCommand("ms-endUndoUnit")
@moveCursor(textArea, tag, wrap)
gl.text.moveCursor = (textArea, tag, wrapped) ->
return unless textArea.setSelectionRange
if textArea.selectionStart is textArea.selectionEnd
if wrapped
pos = textArea.selectionStart - tag.length
else
pos = textArea.selectionStart
textArea.setSelectionRange pos, pos
gl.text.updateText = (textArea, tag, wrap) ->
$textArea = $(textArea)
oldVal = $textArea.val()
textArea = $textArea.get(0)
text = $textArea.val()
selected = @selectedText(text, textArea)
$textArea.focus()
@insertText(textArea, text, tag, selected, wrap)
gl.text.init = (form) ->
self = @
$('.js-md', form)
.off 'click'
.on 'click', ->
$this = $(@)
self.updateText(
$this.closest('.md-area').find('textarea'),
$this.data('md-tag'),
not $this.data('md-prepend')
)
gl.text.removeListeners = (form) ->
$('.js-md', form).off()
) window
......@@ -4,18 +4,10 @@ class @Milestone
type: "PUT"
url: issue_url
data: data
success: (data) ->
if data.saved == true
if data.assignee_avatar_url
img_tag = $('<img/>')
img_tag.attr('src', data.assignee_avatar_url)
img_tag.addClass('avatar s16')
$(li).find('.assignee-icon').html(img_tag)
else
$(li).find('.assignee-icon').html('')
$(li).effect 'highlight'
else
new Flash("Issue update failed", 'alert')
success: (_data) =>
@successCallback(_data, li)
error: (data) ->
new Flash("Issue update failed", 'alert')
dataType: "json"
@sortIssues: (data) ->
......@@ -25,9 +17,10 @@ class @Milestone
type: "PUT"
url: sort_issues_url
data: data
success: (data) ->
if data.saved != true
new Flash("Issues update failed", 'alert')
success: (_data) =>
@successCallback(_data)
error: ->
new Flash("Issues update failed", 'alert')
dataType: "json"
@sortMergeRequests: (data) ->
......@@ -37,9 +30,10 @@ class @Milestone
type: "PUT"
url: sort_mr_url
data: data
success: (data) ->
if data.saved != true
new Flash("MR update failed", 'alert')
success: (_data) =>
@successCallback(_data)
error: (data) ->
new Flash("Issue update failed", 'alert')
dataType: "json"
@updateMergeRequest: (li, merge_request_url, data) ->
......@@ -47,20 +41,23 @@ class @Milestone
type: "PUT"
url: merge_request_url
data: data
success: (data) ->
if data.saved == true
if data.assignee_avatar_url
img_tag = $('<img/>')
img_tag.attr('src', data.assignee_avatar_url)
img_tag.addClass('avatar s16')
$(li).find('.assignee-icon').html(img_tag)
else
$(li).find('.assignee-icon').html('')
$(li).effect 'highlight'
else
new Flash("Issue update failed", 'alert')
success: (_data) =>
@successCallback(_data, li)
error: (data) ->
new Flash("Issue update failed", 'alert')
dataType: "json"
@successCallback: (data, element) =>
if data.assignee
img_tag = $('<img/>')
img_tag.attr('src', data.assignee.avatar_url)
img_tag.addClass('avatar s16')
$(element).find('.assignee-icon').html(img_tag)
else
$(element).find('.assignee-icon').html('')
$(element).effect 'highlight'
constructor: ->
oldMouseStart = $.ui.sortable.prototype._mouseStart
$.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
......@@ -81,8 +78,10 @@ class @Milestone
stop: (event, ui) ->
$(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
# Prevents sorting from container which element has been removed.
if $(this).find(ui.item).length > 0
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
receive: (event, ui) ->
new_state = $(this).data('state')
......
......@@ -4,9 +4,6 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require raphael
#= require g.raphael
#= require g.bar
#= require_tree .
$ ->
......
......@@ -102,12 +102,15 @@ class @Notes
keydownNoteText: (e) ->
$this = $(this)
if $this.val() is '' and e.which is 38 #aka the up key
if $this.val() is '' and e.which is 38 and not isMetaKey e
myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
if myLastNote.length
myLastNoteEditBtn = myLastNote.find('.js-note-edit')
myLastNoteEditBtn.trigger('click', [true, myLastNote])
isMetaKey = (e) ->
(e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
......
class @NotificationsDropdown
$ ->
constructor: ->
$(document)
.off 'click', '.update-notification'
.on 'click', '.update-notification', (e) ->
......@@ -18,7 +18,8 @@ class @NotificationsDropdown
.off 'ajax:success', '.notification-form'
.on 'ajax:success', '.notification-form', (e, data) ->
if data.saved
new Flash('Notification settings saved', 'notice')
$(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html)
$(e.currentTarget)
.closest('.notification-dropdown')
.replaceWith(data.html)
else
new Flash('Failed to save new settings', 'alert')
......@@ -19,6 +19,7 @@ class @Project
$('.clone').text(url)
# Ref switcher
@initRefSwitcher()
$('.project-refs-select').on 'change', ->
$(@).parents('form').submit()
......@@ -34,7 +35,6 @@ class @Project
$(@).parents('.no-password-message').remove()
e.preventDefault()
@projectSelectDropdown()
projectSelectDropdown: ->
......@@ -50,3 +50,42 @@ class @Project
changeProject: (url) ->
window.location = url
initRefSwitcher: ->
$('.js-project-refs-dropdown').each ->
$dropdown = $(@)
selected = $dropdown.data('selected')
$dropdown.glDropdown(
data: (term, callback) ->
$.ajax(
url: $dropdown.data('refs-url')
data:
ref: $dropdown.data('ref')
).done (refs) ->
callback(refs)
selectable: true
filterable: true
filterByText: true
fieldName: 'ref'
renderRow: (ref) ->
if ref.header?
$('<li />')
.addClass('dropdown-header')
.text(ref.header)
else
link = $('<a />')
.attr('href', '#')
.addClass(if ref is selected then 'is-active' else '')
.text(ref)
.attr('data-ref', escape(ref))
$('<li />')
.append(link)
id: (obj, $el) ->
$el.attr('data-ref')
toggleLabel: (obj, $el) ->
$el.text().trim()
clicked: (e) ->
$dropdown.closest('form').submit()
)
# This is a manifest file that'll be compiled into including all the files listed below.
# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
# be included in the compiled file accessible from http://example.com/assets/application.js
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require d3
#= require_tree .
......@@ -37,3 +37,4 @@
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
@import "framework/blank";
.blank-state {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
}
.blank-state-no-icon {
padding-top: 40px;
padding-bottom: 40px;
}
.blank-state-title {
margin-top: 0;
margin-bottom: 5px;
font-size: 19px;
font-weight: normal;
}
.blank-state-text {
margin-top: 0;
margin-bottom: $gl-padding;
font-size: 15px;
}
......@@ -97,6 +97,22 @@
}
}
.sub-header-block {
background-color: $white-light;
border-bottom: 1px solid $white-dark;
padding: 11px 0;
margin-bottom: 11px;
.oneline {
line-height: 35px;
}
&.no-bottom-space {
border-bottom: 0;
margin-bottom: 0;
}
}
.cover-block {
text-align: center;
background: $background-color;
......
......@@ -461,10 +461,12 @@
}
}
.ui-state-active,
.ui-state-hover {
color: $md-link-color;
background-color: $calendar-hover-bg;
.ui-datepicker-calendar {
.ui-state-hover,
.ui-state-active {
color: #fff;
border: 0;
}
}
.ui-datepicker-prev,
......
......@@ -26,7 +26,6 @@ header {
text-align: center;
#tanuki-logo, img {
width: 36px;
height: 36px;
}
}
......@@ -132,6 +131,10 @@ header {
transition-duration: .3s;
z-index: 999;
svg, img {
height: 36px;
}
&:hover {
cursor: pointer;
}
......
......@@ -65,6 +65,11 @@
a {
padding-top: 0;
line-height: 1;
border-bottom: 1px solid $border-color;
&.btn.btn-xs {
padding: 2px 5px;
}
}
}
}
......@@ -97,5 +102,30 @@
white-space: pre-wrap;
word-break: keep-all;
}
@include bulleted-list;
}
}
.toolbar-group {
float: left;
margin-right: -5px;
margin-left: $gl-padding;
&:first-child {
margin-left: 0;
}
}
.toolbar-btn {
float: left;
padding: 0 5px;
color: #959494;
background: transparent;
border: 0;
outline: 0;
&:hover {
color: $gl-link-color;
}
}
......@@ -110,3 +110,17 @@
font-size: 16px;
line-height: 24px;
}
@mixin bulleted-list {
> ul {
list-style-type: disc;
ul {
list-style-type: circle;
ul {
list-style-type: square;
}
}
}
}
\ No newline at end of file
@mixin fade($gradient-direction, $rgba, $gradient-color) {
visibility: visible;
opacity: 1;
visibility: hidden;
opacity: 0;
z-index: 2;
position: absolute;
bottom: 12px;
......@@ -13,11 +13,18 @@
background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
&.end-scroll {
visibility: hidden;
opacity: 0;
&.scrolling {
visibility: visible;
opacity: 1;
transition-duration: .3s;
}
.fa {
position: relative;
top: 3px;
font-size: 13px;
color: $btn-placeholder-gray;
}
}
@mixin scrolling-links() {
......@@ -25,6 +32,7 @@
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
}
......@@ -104,10 +112,6 @@
width: 50%;
line-height: 28px;
&.wiki-page {
padding: 16px 10px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
width: 100%;
......@@ -136,7 +140,7 @@
}
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-max) {
@media (max-width: $screen-xs-max) {
width: 100%;
}
}
......@@ -220,6 +224,7 @@
form {
display: block;
height: auto;
margin-bottom: 14px;
input {
width: 100%;
......@@ -268,7 +273,7 @@
float: right;
padding: 7px 0 0;
@media (max-width: $screen-xs-max) {
@media (max-width: $screen-sm-max) {
display: none;
}
......@@ -299,33 +304,9 @@
}
.nav-links {
@include scrolling-links();
border-bottom: none;
height: 51px;
svg {
position: relative;
top: 2px;
margin-right: 2px;
height: 15px;
width: auto;
path,
polygon {
fill: $layout-link-gray;
}
}
.fade-right {
@include fade(left, rgba(250, 250, 250, 0.4), $background-color);
right: 0;
}
.fade-left {
@include fade(right, rgba(250, 250, 250, 0.4), $background-color);
left: 0;
}
li {
a {
......@@ -361,18 +342,6 @@
}
}
}
.nav-control {
.fade-right {
@media (min-width: $screen-xs-max) {
right: 68px;
}
@media (max-width: $screen-xs-min) {
right: 0;
}
}
}
}
.scrolling-tabs-container {
......@@ -380,15 +349,42 @@
.nav-links {
@include scrolling-links();
}
.fade-right {
@include fade(left, rgba(255, 255, 255, 0.4), $background-color);
right: -5px;
.fa {
right: -7px;
}
}
.fade-left {
@include fade(right, rgba(255, 255, 255, 0.4), $background-color);
left: -5px;
.fa {
left: -7px;
}
}
&.sub-nav-scroll {
.fade-right {
@include fade(left, rgba(255, 255, 255, 0.4), $background-color);
right: 0;
.fa {
right: -23px;
}
}
.fade-left {
@include fade(right, rgba(255, 255, 255, 0.4), $background-color);
left: 0;
.fa {
left: 10px;
}
}
}
}
......@@ -401,21 +397,19 @@
.fade-right {
@include fade(left, rgba(255, 255, 255, 0.4), $white-light);
right: 0;
right: -5px;
.fa {
right: -7px;
}
}
.fade-left {
@include fade(right, rgba(255, 255, 255, 0.4), $white-light);
left: 0;
}
&.event-filter {
.fade-right {
visibility: hidden;
left: -5px;
@media (max-width: $screen-xs-max) {
visibility: visible;
}
.fa {
left: -7px;
}
}
}
......
......@@ -9,6 +9,10 @@
margin-top: -2px;
float: right;
}
.dropdown-menu-toggle {
line-height: 20px;
}
}
.panel-body {
......
......@@ -165,11 +165,6 @@
background-size: 16px 16px !important;
}
/** Branch/tag selector **/
.project-refs-form .select2-container {
width: 160px !important;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
......
......@@ -91,7 +91,6 @@
text-decoration: none;
font-weight: normal;
outline: none;
white-space: nowrap;
&:hover,
&:active,
......
......@@ -8,8 +8,9 @@
.emoji-menu {
position: absolute;
margin-top: 3px;
z-index: 1000;
min-width: 160px;
padding: $gl-padding;
z-index: 9;
width: 300px;
font-size: 14px;
background-color: $award-emoji-menu-bg;
border: 1px solid $award-emoji-menu-border;
......@@ -33,20 +34,18 @@
}
.emoji-menu-content {
padding: $gl-padding;
width: 300px;
height: 300px;
overflow-y: scroll;
input.emoji-search {
background-image: url("data:image/png;base64,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");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
}
}
}
.emoji-search {
background-image: url("data:image/png;base64,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");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
}
.emoji-menu-list {
list-style: none;
padding-left: 0;
......
......@@ -80,9 +80,14 @@
.commit {
padding: 10px 0;
position: relative;
@media (min-width: $screen-sm-min) {
padding-left: 46px;
padding-left: 20px;
.commit-info-block {
padding-left: 44px;
}
}
&:not(:last-child) {
......@@ -95,8 +100,11 @@
vertical-align: baseline;
}
.avatar {
margin-left: -46px;
position: absolute;
top: 10px;
left: 16px;
}
.item-title {
......
......@@ -4,6 +4,11 @@
margin-bottom: $gl-padding;
border-radius: 3px;
.commit-short-id {
font-family: $regular_font;
font-weight: 400;
}
.diff-header {
position: relative;
background: $background-color;
......
......@@ -60,13 +60,14 @@
.encoding-selector,
.license-selector,
.gitignore-selector {
.gitignore-selector,
.gitlab-ci-yml-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
}
.gitignore-selector, .license-selector {
.gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
}
......@@ -76,4 +77,10 @@
width: 220px;
}
}
.gitlab-ci-yml-selector {
.dropdown-menu-toggle {
width: 250px;
}
}
}
......@@ -54,6 +54,10 @@
}
}
code {
white-space: pre-wrap;
}
pre {
border: none;
background: #f9f9f9;
......
......@@ -57,4 +57,12 @@
.documentation {
padding: 7px;
// Border around images in the help pages.
img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px);
}
}
......@@ -4,6 +4,14 @@
margin-right: 1px;
}
}
// Border around images in issue and MR descriptions.
.description img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px);
}
}
.issuable-filter-count {
......
......@@ -6,6 +6,7 @@
height: 30px;
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
}
&.suggest-colors-dropdown {
......@@ -50,11 +51,10 @@
.label-row {
.label-name {
display: block;
display: inline-block;
margin-bottom: 10px;
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 200px;
margin-bottom: 0;
}
......@@ -63,6 +63,7 @@
.label-description {
display: block;
margin-bottom: 10px;
margin-left: 50px;
@media (min-width: $screen-sm-min) {
display: inline-block;
......
......@@ -119,7 +119,12 @@
margin-bottom: 0;
}
@media (max-width: $screen-sm-max) {
.btn-grouped {
margin-left: 0;
margin-right: 7px;
}
@media (max-width: $screen-xs-max) {
h4 {
font-size: 15px;
}
......@@ -131,10 +136,14 @@
.btn,
.btn-group,
.accept-action {
width: 100%;
margin-bottom: 4px;
}
.accept-action {
width: 100%;
text-align: center;
}
.accept-control {
width: 100%;
text-align: center;
......@@ -284,7 +293,7 @@
margin-bottom: 0;
}
@media (min-width: $screen-sm-min) {
@media (min-width: $screen-xs-min) {
float: left;
width: 50%;
margin-bottom: 0;
......
......@@ -179,6 +179,10 @@
border-top: 1px solid $border-color;
}
.md-helper {
padding-top: 10px;
}
.toolbar-button {
padding: 0;
background: none;
......@@ -219,3 +223,16 @@
float: left;
}
}
.note-form-actions {
@media (max-width: $screen-xs-max) {
.btn {
float: none;
width: 100%;
&:not(:last-child) {
margin-bottom: 10px;
}
}
}
}
......@@ -84,24 +84,14 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
// On diffs code should wrap nicely and not overflow
code {
white-space: pre-wrap;
}
// Reset ul style types since we're nested inside a ul already
& > ul {
list-style-type: disc;
ul {
list-style-type: circle;
ul {
list-style-type: square;
}
}
}
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
......@@ -117,6 +107,14 @@ ul.notes {
code {
word-break: keep-all;
}
// Border around images in issue and MR comments.
img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px 0;
max-height: calc(100vh - 100px);
}
}
}
......
......@@ -101,7 +101,8 @@
.notifications-btn {
.fa-bell {
.fa-bell,
.fa-spinner {
margin-right: 6px;
}
......@@ -373,7 +374,7 @@ a.deploy-project-label {
.project-stats {
margin-top: $gl-padding;
margin-bottom: 0;
padding: 16px 0;
padding: 0;
background-color: $white-light;
font-size: 0;
......@@ -382,13 +383,14 @@ a.deploy-project-label {
}
.nav li {
display: inline;
display: inline-block;
margin: 16px 0;
margin-right: 16px;
}
.nav > li > a {
background-color: transparent;
margin-right: 12px;
padding: 0 10px;
padding: 5px 10px;
font-size: 15px;
color: $notes-light-color;
}
......@@ -402,12 +404,17 @@ a.deploy-project-label {
font-size: 17px;
}
li.missing a {
color: #5a6069;
border: 1px dashed #dce0e5;
li.missing {
border: 1px dashed $border-gray-light;
border-radius: $border-radius-default;
a {
color: $notes-light-color;
display: block;
}
&:hover {
background-color: #f0f2f5;
background-color: $gray-normal;
}
}
......@@ -616,3 +623,9 @@ pre.light-well {
color: $gl-text-green;
}
}
.project-refs-form {
.dropdown-menu {
width: 300px;
}
}
......@@ -14,24 +14,38 @@
font-size: 10px;
}
#contributors-master {
@include make-md-column(12);
svg {
width: 100%;
}
}
#contributors {
.contributors-list {
margin: 0 0 10px;
list-style: none;
padding: 0;
svg {
width: 100%;
}
}
.person {
&:nth-child(even) {
float: right;
}
float: left;
@include make-md-column(6);
margin-top: 10px;
@media (max-width: $screen-sm-min) {
width: 100%;
}
}
.person .spark {
display: block;
background: #f3f3f3;
width: 100%;
}
.person .area-contributor {
......
......@@ -62,6 +62,10 @@
}
}
code {
white-space: pre-wrap;
}
pre {
border: none;
background: #f9f9f9;
......
......@@ -5,6 +5,7 @@ class Admin::AppearancesController < Admin::ApplicationController
end
def preview
render 'preview', layout: 'devise'
end
def create
......
class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
def index
@runner_projects = project.runner_projects.all
@runner_project = project.runner_projects.new
end
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
if @runner.assign_to(@project, current_user)
return head(403) if @runner.is_shared? || @runner.locked?
runner_project = @runner.assign_to(@project, current_user)
if runner_project.persisted?
redirect_to admin_runner_path(@runner)
else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
......
......@@ -36,6 +36,10 @@ class ApplicationController < ActionController::Base
render_404
end
rescue_from Gitlab::Access::AccessDeniedError do |exception|
render_403
end
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
......
......@@ -21,29 +21,18 @@ module MembershipActions
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
render_403
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
end
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
redirect_to redirect_path, notice: notice
end
protected
......@@ -51,8 +40,4 @@ module MembershipActions
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
end
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
@group_members = current_user.group_members.page(params[:page])
@group_members = current_user.group_members.includes(:source).page(params[:page])
end
end
......@@ -36,9 +36,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def destroy
@group_member = @group.group_members.find(params[:id])
return render_403 unless can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......@@ -68,8 +66,4 @@ class Groups::GroupMembersController < Groups::ApplicationController
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end
......@@ -12,9 +12,13 @@ class Import::GitlabProjectsController < Import::BaseController
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
imported_file = project_params[:file].path + "-import"
FileUtils.copy_entry(project_params[:file].path, imported_file)
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
File.expand_path(project_params[:file].path),
File.expand_path(imported_file),
project_params[:path]).execute
if @project.saved?
......
......@@ -74,7 +74,7 @@ class Projects::ApplicationController < ApplicationController
end
def require_branch_head
unless @repository.branch_names.include?(@ref)
unless @repository.branch_exists?(@ref)
redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref),
notice: "This action is not allowed unless you are on a branch"
......
......@@ -16,6 +16,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :from_merge_request, only: [:edit, :update]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
def new
commit unless @repository.empty?
......@@ -146,4 +147,10 @@ class Projects::BlobController < Projects::ApplicationController
file_content_encoding: params[:encoding]
}
end
def validate_diff_params
if [:since, :to, :offset].any? { |key| params[key].blank? }
render nothing: true
end
end
end
......@@ -18,9 +18,16 @@ class Projects::CommitController < Projects::ApplicationController
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)
@notes = commit.notes.non_diff_notes.fresh
@noteable = @commit
@comments_target = {
noteable_type: 'Commit',
......
......@@ -62,8 +62,12 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
raw_notes = @issue.notes_with_associations.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.with_associations.fresh
@noteable = @issue
respond_to do |format|
......@@ -111,6 +115,7 @@ class Projects::IssuesController < Projects::ApplicationController
render :edit
end
end
format.json do
render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
end
......
......@@ -85,6 +85,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@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.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
......@@ -190,7 +199,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
unless @merge_request.mergeable?
# Disable the CI check if merge_when_build_succeeds is enabled since we have
# to wait until CI completes to know
unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?)
@status = :failed
return
end
......@@ -204,8 +215,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
if @merge_request.pipeline && @merge_request.pipeline.active?
if params[:merge_when_build_succeeds].present?
unless @merge_request.pipeline
@status = :failed
return
end
if @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
......@@ -320,8 +336,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh
@discussions = @notes.discussions
@discussions = @merge_request.mr_and_commit_notes.
inc_author_project_award_emoji.
fresh.
discussions
@notes = Banzai::NoteRenderer.render(
@discussions.flatten,
@project,
current_user,
@path,
@project_wiki,
@ref
)
@noteable = @merge_request
# Get commits from repository
......@@ -368,4 +397,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ensure_ref_fetched
@merge_request.ensure_ref_fetched
end
def merge_when_build_succeeds_active?
params[:merge_when_build_succeeds].present? &&
@merge_request.pipeline && @merge_request.pipeline.active?
end
end
......@@ -24,6 +24,10 @@ class Projects::NotesController < Projects::ApplicationController
def create
@note = Notes::CreateService.new(project, current_user, note_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
......@@ -33,6 +37,10 @@ class Projects::NotesController < Projects::ApplicationController
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
......@@ -118,6 +126,8 @@ class Projects::NotesController < Projects::ApplicationController
name: note.name
}
elsif note.valid?
Banzai::NoteRenderer.render([note], @project, current_user)
{
valid: true,
id: note.id,
......
......@@ -54,6 +54,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def commit
@commit ||= @pipeline.commit_data
@commit ||= @pipeline.commit
end
end
......@@ -50,9 +50,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def destroy
@project_member = @project.project_members.find(params[:id])
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format|
format.html do
......@@ -98,8 +96,4 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end
......@@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
return head(403) if @runner.is_shared? || @runner.locked?
return head(403) unless current_user.ci_authorized_runners.include?(@runner)
path = runners_path(project)
runner_project = @runner.assign_to(project, current_user)
if @runner.assign_to(project, current_user)
if runner_project.persisted?
redirect_to path
else
redirect_to path, alert: 'Failed adding runner to project'
......
......@@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
@runners = project.runners.ordered
@specific_runners = current_user.ci_authorized_runners.
where.not(id: project.runners).
ordered.page(params[:page]).per(20)
@project_runners = project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners.
assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
......
class ProjectsController < Projects::ApplicationController
include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity]
before_action :authenticate_user!, except: [:show, :activity, :refs]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
......@@ -251,6 +251,24 @@ class ProjectsController < Projects::ApplicationController
}
end
def refs
options = {
'Branches' => @repository.branch_names,
}
unless @repository.tag_count.zero?
options['Tags'] = VersionSorter.rsort(@repository.tag_names)
end
# If reference is commit id - we should add it to branch/tag selectbox
ref = Addressable::URI.unescape(params[:ref])
if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
options['Commits'] = [ref]
end
render json: options.to_json
end
private
def determine_layout
......@@ -285,8 +303,14 @@ class ProjectsController < Projects::ApplicationController
project.repository_exists? && !project.empty_repo?
end
# Override get_id from ExtractsPath, which returns the branch and file path
# Override extract_ref from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
# This way we avoid to access the repository.ref_names.
def extract_ref(_id)
[get_id, '']
end
# Override get_id from ExtractsPath in this case is just the root of the default branch.
def get_id
project.repository.root_ref
end
......
......@@ -101,22 +101,6 @@ module ApplicationHelper
'Never'
end
def grouped_options_refs
repository = @project.repository
options = [
['Branches', repository.branch_names],
['Tags', VersionSorter.rsort(repository.tag_names)]
]
# 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/
options << ['Commit', [@ref]]
end
grouped_options_for_select(options, @ref || @project.default_branch)
end
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
......@@ -132,7 +116,7 @@ module ApplicationHelper
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that
return false unless project.repository.branch_names.include?(event.branch_name)
return false unless project.repository.branch_exists?(event.branch_name)
true
end
......@@ -213,7 +197,7 @@ module ApplicationHelper
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.preserve(markdown(file_content))
Hamlit::RailsHelpers.preserve(markdown(file_content))
elsif asciidoc?(file_name)
asciidoc(file_content)
elsif plain?(file_name)
......
module BlobHelper
def highlighter(blob_name, blob_content, nowrap: false)
Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap)
def highlighter(blob_name, blob_content, repository: nil, nowrap: false)
Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap, repository: repository)
end
def highlight(blob_name, blob_content, nowrap: false, plain: false)
Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain)
def highlight(blob_name, blob_content, repository: nil, nowrap: false, plain: false)
Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain, repository: repository)
end
def no_highlight_files
......@@ -29,7 +29,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn btn-file-option'
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
......@@ -186,12 +186,16 @@ module BlobHelper
end
def gitignore_names
return @gitignore_names if defined?(@gitignore_names)
@gitignore_names ||=
Gitlab::Template::Gitignore.categories.keys.map do |k|
[k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
end.to_h
end
@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 } }
}
def gitlab_ci_ymls
@gitlab_ci_ymls ||=
Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
[k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
end.to_h
end
end
......@@ -10,7 +10,7 @@ module BranchesHelper
end
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name)
return false unless project.repository.branch_exists?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
......
......@@ -50,8 +50,6 @@ module GitlabMarkdownHelper
context[:project] ||= @project
text = Banzai.pre_process(text, context)
html = Banzai.render(text, context)
context.merge!(
......@@ -185,4 +183,17 @@ module GitlabMarkdownHelper
''
end
end
def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" })
content_tag :button,
type: "button",
class: "toolbar-btn js-md has-tooltip hidden-xs",
tabindex: -1,
data: data,
title: options[:title],
aria: { label: options[:title] } do
icon(options[:icon])
end
end
end
module JavascriptHelper
def page_specific_javascripts(js = nil)
@page_specific_javascripts = js unless js.nil?
@page_specific_javascripts
def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js), { "data-turbolinks-track" => true }
end
end
......@@ -12,6 +12,11 @@ module Emails
@member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
# A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group
admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
end
mail(to: admins,
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
......
......@@ -196,7 +196,8 @@ class Ability
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project,
:read_commit_status
:read_commit_status,
:read_pipeline
]
end
......
......@@ -300,18 +300,12 @@ module Ci
project.valid_runners_token? token
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) }
project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
end
def stuck?
......
......@@ -37,22 +37,22 @@ module Ci
end
def git_author_name
commit_data.author_name if commit_data
commit.try(:author_name)
end
def git_author_email
commit_data.author_email if commit_data
commit.try(:author_email)
end
def git_commit_message
commit_data.message if commit_data
commit.try(:message)
end
def short_sha
Ci::Pipeline.truncate_sha(sha)
end
def commit_data
def commit
@commit ||= project.commit(sha)
rescue
nil
......@@ -163,13 +163,26 @@ module Ci
end
def skip_ci?
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message
end
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
# them using the +Gitlab::ImportExport::RelationFactory+ class.
def notes=(notes)
notes.each do |note|
note[:id] = nil
note[:commit_id] = sha
note[:noteable_id] = self['id']
note.save!
end
end
def notes
Note.for_commit_id(sha)
end
......
......@@ -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 run_untagged]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
......@@ -26,6 +26,13 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
where(locked: false).
where.not("id IN (#{project.runners.select(:id).to_sql})").specific
end
validate :tag_constraints
acts_as_taggable
......@@ -56,7 +63,7 @@ module Ci
def assign_to(project, current_user = nil)
self.is_shared = false if shared?
self.save
project.runner_projects.create!(runner_id: self.id)
project.runner_projects.create(runner_id: self.id)
end
def display_name
......@@ -91,6 +98,10 @@ module Ci
!shared?
end
def can_pick?(build)
assignable_for?(build.project) && accepting_tags?(build)
end
def only_for?(project)
projects == [project]
end
......@@ -111,5 +122,13 @@ module Ci
'can not be empty when runner is not allowed to pick untagged jobs')
end
end
def assignable_for?(project)
!locked? || projects.exists?(id: project.id)
end
def accepting_tags?(build)
(run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
end
end
end
......@@ -13,6 +13,7 @@ module Ci
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
end
......
......@@ -271,6 +271,32 @@ class Commit
merged_merge_request ? 'merge request' : 'commit'
end
# Get the URI type of the given path
#
# Used to build URLs to files in the repository in GFM.
#
# path - String path to check
#
# Examples:
#
# uri_type('doc/README.md') # => :blob
# uri_type('doc/logo.png') # => :raw
# uri_type('doc/api') # => :tree
# uri_type('not/found') # => :nil
#
# Returns a symbol
def uri_type(path)
entry = @raw.tree.path(path)
if entry[:type] == :blob
blob = Gitlab::Git::Blob.new(name: entry[:name])
blob.image? ? :raw : :blob
else
entry[:type]
end
rescue Rugged::TreeError
nil
end
private
def repo_changes
......
......@@ -8,6 +8,8 @@ class CommitStatus < ActiveRecord::Base
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
delegate :commit, to: :pipeline
validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name
......
......@@ -2,10 +2,11 @@ module Awardable
extend ActiveSupport::Concern
included do
has_many :award_emoji, as: :awardable, dependent: :destroy
has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy
if self < Participable
participant :award_emoji_with_associations
# By default we always load award_emoji user association
participant :award_emoji
end
end
......@@ -34,12 +35,9 @@ module Awardable
end
end
def award_emoji_with_associations
award_emoji.includes(:user)
end
def grouped_awards(with_thumbs: true)
awards = award_emoji_with_associations.group_by(&:name)
# By default we always load award_emoji user association
awards = award_emoji.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= []
......
......@@ -19,9 +19,14 @@ module Issuable
belongs_to :milestone
has_many :notes, as: :noteable, dependent: :destroy do
def authors_loaded?
# We check first if we're loaded to not load unnecesarily.
# We check first if we're loaded to not load unnecessarily.
loaded? && to_a.all? { |note| note.association(:author).loaded? }
end
def award_emojis_loaded?
# We check first if we're loaded to not load unnecessarily.
loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
end
end
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
......@@ -49,7 +54,7 @@ module Issuable
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
scope :inc_notes_with_associations, -> { includes(notes: :author) }
scope :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) }
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
......@@ -112,15 +117,18 @@ module Issuable
end
def sort(method, excluded_labels: [])
case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
else
order_by(method)
end
sorted = case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
else
order_by(method)
end
# Break ties with the ID column for pagination
sorted.order(id: :desc)
end
def order_labels_priority(excluded_labels: [])
......@@ -257,7 +265,14 @@ module Issuable
# already have their authors loaded (possibly because the scope
# `inc_notes_with_associations` was used) and skip the inclusion if that's
# the case.
notes.authors_loaded? ? notes : notes.includes(:author)
includes = []
includes << :author unless notes.authors_loaded?
includes << :award_emoji unless notes.award_emojis_loaded?
if includes.any?
notes.includes(includes)
else
notes
end
end
def updated_tasks
......
......@@ -53,6 +53,16 @@ module Participable
#
# Returns an Array of User instances.
def participants(current_user = nil)
@participants ||= Hash.new do |hash, user|
hash[user] = raw_participants(user)
end
@participants[current_user]
end
private
def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
participants = Set.new
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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