Commit cf4ccdda authored by Mike Wyatt's avatar Mike Wyatt

Merge remote-tracking branch 'upstream/master' into better-asana-refs

* upstream/master: (307 commits)
  Update CHANGELOG
  spinach fix
  Updated allocations Gem to version 1.0.3
  Removed various default metrics tags
  Update CHANGELOG
  Fix "I see current user as the first user" step
  Swap Author and Assignee Selectors on issuable index view
  Update CHANGELOG
  Make sure that is no pending migrations in Gitlab::CurrentSettings
  Added additional config environmental variables to help Debian packaging
  We don't use whenever anymore. Lets remove the schedule file
  Fix project transfer e-mail sending incorrect paths in e-mail notification
  Update CHANGELOG
  Use Gitlab::CurrentSettings for InfluxDB
  Write to InfluxDB directly via UDP
  Strip newlines from obfuscated SQL
  Add hotfix that allows to access build artifacts created before 8.3
  note votes methids implementation
  When reCAPTCHA is disabled, allow registrations to go through without a code
  Downcased user or email search for avatar_icon.
  ...
parents 571df5f4 d33cc4e5
......@@ -12,6 +12,7 @@ before_script:
spec:feature:
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
......
......@@ -76,7 +76,7 @@ Style/BlockEndNewline:
Description: 'Put end statement of multiline block on its own line.'
Enabled: true
Style/Blocks:
Style/BlockDelimiters:
Description: >-
Avoid using {...} for multi-line blocks (multiline chaining is
always ugly).
......@@ -232,6 +232,10 @@ Style/EvenOdd:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
Enabled: false
Style/ExtraSpacing:
Description: 'Do not use unnecessary spacing.'
Enabled: false
Style/FileName:
Description: 'Use snake_case for source file names.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
......@@ -431,6 +435,14 @@ Style/OpMethod:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
Enabled: false
Style/ParallelAssignment:
Description: >-
Check for simple usages of parallel assignment.
It will only warn when the number of variables
matches on both sides of the assignment.
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment'
Enabled: false
Style/ParenthesesAroundCondition:
Description: >-
Don't use parentheses around the condition of an
......@@ -669,6 +681,13 @@ Style/TrailingWhitespace:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
Enabled: false
Style/TrailingUnderscoreVariable:
Description: >-
Checks for the usage of unneeded trailing underscores at the
end of parallel variable assignment.
AllowNamedUnderscoreVariables: true
Enabled: false
Style/TrivialAccessors:
Description: 'Prefer attr_* methods to trivial readers/writers.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
......@@ -690,11 +709,6 @@ Style/UnneededPercentQ:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
Enabled: false
Style/UnneededPercentX:
Description: 'Checks for %x when `` would do.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x'
Enabled: false
Style/VariableInterpolation:
Description: >-
Don't interpolate global, instance and class variables
......@@ -778,6 +792,10 @@ Metrics/MethodLength:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
Enabled: false
Metrics/ModuleLength:
Description: 'Avoid modules longer than 100 lines of code.'
Enabled: false
#################### Lint ################################
### Warnings
......@@ -961,6 +979,12 @@ Rails/ActionFilter:
Description: 'Enforces consistent use of action filter methods.'
Enabled: true
Rails/Date:
Description: >-
Checks the correct usage of date aware methods,
such as Date.today, Date.current etc.
Enabled: false
Rails/DefaultScope:
Description: 'Checks if the argument passed to default_scope is a block.'
Enabled: false
......@@ -987,6 +1011,12 @@ Rails/ScopeArgs:
Description: 'Checks the arguments of ActiveRecord scopes.'
Enabled: false
Rails/TimeZone:
Description: 'Checks the correct usage of time zone aware methods.'
StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time'
Reference: 'http://danilenko.org/2012/7/6/rails_timezones'
Enabled: false
Rails/Validation:
Description: 'Use validates :attribute, hash of validations.'
Enabled: false
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased)
v 8.4.0 (unreleased)
- Implement new UI for group page
- Implement search inside emoji picker
- Add API support for looking up a user by username (Stan Hu)
- Add project permissions to all project API endpoints (Stan Hu)
- Only allow group/project members to mention `@all`
- Expose Git's version in the admin area (Trey Davis)
- Add "Frequently used" category to emoji picker
- Add CAS support (tduehr)
- Add link to merge request on build detail page
- Revert back upvote and downvote button to the issue and MR pages
- Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg)
v 8.3.3 (unreleased)
- Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
- Enable "Add key" button when user fills in a proper key (Stan Hu)
v 8.3.2
- Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
- Add support for Google reCAPTCHA in user registration
v 8.3.1
- Fix Error 500 when global milestones have slashes (Stan Hu)
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Fix LDAP identity and user retrieval when special characters are used
- Move Sidekiq-cron configuration to gitlab.yml
- Enable forcing Two-Factor authentication sitewide, with optional grace period
v 8.3.0
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
- API support for starred projects for authorized user (Zeger-Jan van de Weg)
- Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
- Merge when build succeeds (Zeger-Jan van de Weg)
- Provide better diagnostic message upon project creation errors (Stan Hu)
- Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
- Remove api credentials from link to build_page
- Deprecate GitLabCiService making it to always be inactive
- Bump gollum-lib to 4.1.0 (Stan Hu)
- Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu)
......@@ -17,13 +49,16 @@ v 8.3.0 (unreleased)
- Fix 500 error when update group member permission
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
- Recognize issue/MR/snippet/commit links as references
- Backport JIRA features from EE to CE
- Add ignore whitespace change option to commit view
- Fire update hook from GitLab
- Allow account unlock via email
- Style warning about mentioning many people in a comment
- Fix: sort milestones by due date once again (Greg Smethells)
- Migrate all CI::Services and CI::WebHooks to Services and WebHooks
- Don't show project fork event as "imported"
- Add API endpoint to fetch merge request commits list
- Don't create CI status for refs that doesn't have .gitlab-ci.yml, even if the builds are enabled
- Expose events API with comment information and author info
- Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583
- Run custom Git hooks when branch is created or deleted.
......@@ -62,8 +97,6 @@ v 8.2.3
- Update documentation for "Guest" permissions
- Properly convert Emoji-only comments into Award Emojis
- Enable devise paranoid mode to prevent user enumeration attack
v 8.2.3
- Webhook payload has an added, modified and removed properties for each commit
- Fix 500 error when creating a merge request that removes a submodule
......
......@@ -155,6 +155,28 @@ sudo -u git -H bundle exec rake gitlab:env:info)
```
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
one or multiple issues. This makes it possible to schedule work more accurately.
You are encouraged to set the weight of any issue. Following the guidelines
below will make it easy to manage this, without unnecessary overhead.
1. Set weight for any issue at the earliest possible convenience
1. If you don't agree with a set weight, discuss with other developers until
consensus is reached about the weight
1. Issue weights are an abstract measurement of complexity of the issue. Do not
relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring)
and something you want to avoid.
1. Something that has a weight of 1 (or no weight) is really small and simple.
Something that is 9 is rewriting a large fundamental part of GitLab,
which might lead to many hard problems to solve. Changing some text in GitLab
is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
1. If something is very large, it should probably be split up in multiple
issues or chunks. You can simply not set the weight of a parent issue and set
weights to children issues.
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
......@@ -358,7 +380,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[core team]: https://about.gitlab.com/core-team/
[getting help page]: https://about.gitlab.com/getting-help/
[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up+for+grabs
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
......
......@@ -23,6 +23,7 @@ gem 'devise-async', '~> 0.9.0'
gem 'doorkeeper', '~> 2.2.0'
gem 'omniauth', '~> 1.2.2'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 3.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
......@@ -34,6 +35,9 @@ gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd'
gem 'rack-oauth2', '~> 1.2.1'
# reCAPTCHA protection
gem 'recaptcha', require: 'recaptcha/rails'
# Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
......@@ -101,6 +105,9 @@ gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 1.10.1'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
gem 'nokogiri', '1.6.7.1'
# Diffs
gem 'diffy', '~> 3.0.3'
......@@ -168,14 +175,14 @@ gem 'd3_rails', '~> 3.5.5'
gem "cal-heatmap-rails", "~> 0.0.1"
# underscore-rails
gem "underscore-rails", "~> 1.4.4"
gem "underscore-rails", "~> 1.8.0"
# Sanitize user input
gem "sanitize", '~> 2.0'
gem 'babosa', '~> 1.0.2'
# Protect against bruteforcing
gem "rack-attack", '~> 4.3.0'
gem "rack-attack", '~> 4.3.1'
# Ace editor
gem 'ace-rails-ap', '~> 2.0.1'
......@@ -186,7 +193,7 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
gem "sass-rails", '~> 4.0.5'
gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
......@@ -198,9 +205,9 @@ gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.2.0'
gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 3.1.3'
gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 4.2.1'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'nprogress-rails', '~> 0.1.6.7'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
......@@ -208,14 +215,22 @@ gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
gem 'connection_pool', '~> 2.0', require: false
end
group :development do
gem "foreman"
gem 'brakeman', '3.0.1', require: false
gem 'brakeman', '~> 3.1.0', require: false
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
gem 'quiet_assets', '~> 1.0.2'
gem 'rerun', '~> 0.10.0'
gem 'rerun', '~> 0.11.0'
gem 'bullet', require: false
gem 'rblineprof', platform: :mri, require: false
gem 'web-console', '~> 2.0'
......@@ -251,7 +266,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.6.0'
gem 'poltergeist', '~> 1.8.1'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
......@@ -261,7 +276,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.28.0', require: false
gem 'rubocop', '~> 0.35.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false
gem 'flog', require: false
......
This diff is collapsed.
8.3.0.pre
8.4.0.pre
......@@ -5,7 +5,7 @@
# the compiled file.
#
#= require jquery
#= require jquery.ui.all
#= require jquery-ui
#= require jquery_ujs
#= require jquery.cookie
#= require jquery.endless-scroll
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event)->
event.stopPropagation()
event.preventDefault()
$(".emoji-menu").show()
$("html").click ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide()
@renderFrequentlyUsedBlock()
@setupSearch()
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
$(".emoji-menu").hide()
addAwardToEmojiBar: (emoji, custom_path = '') ->
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
emoji = @normilizeEmojiName(emoji)
if @exist(emoji)
if @isActive(emoji)
......@@ -17,7 +33,7 @@ class @AwardsHandler
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
else
@createEmoji(emoji, custom_path)
@createEmoji(emoji)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
......@@ -27,15 +43,19 @@ class @AwardsHandler
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
counter.parent().removeClass("active")
emojiIcon.removeClass("active")
@removeMeFromAuthorList(emoji)
else if emoji =="thumbsup" || emoji == "thumbsdown"
emojiIcon.tooltip("destroy")
counter.text(0)
emojiIcon.removeClass("active")
else
award = counter.parent()
award.tooltip("destroy")
award.remove()
emojiIcon.tooltip("destroy")
emojiIcon.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
......@@ -54,35 +74,39 @@ class @AwardsHandler
resetTooltip: (award) ->
award.tooltip("destroy")
# "destroy" call is asynchronous, this is why we need to set timeout.
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
createEmoji: (emoji, custom_path) ->
createEmoji: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon' data-emoji='" + emoji + "'>")
nodes.push(@getImage(emoji, custom_path))
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
nodes.push("<div class='counter'>1</div>")
nodes.push("</div>")
nodes.push("<div class='counter'>1")
nodes.push("</div></div>")
$(".awards-controls").before(nodes.join("\n"))
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
$(".award").tooltip()
getImage: (emoji, custom_path) ->
if custom_path
$("<img>").attr({src: custom_path, width: 20, height: 20}).wrap("<div>").parent().html()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
if emoji_icon.length > 0
unicodeName = emoji_icon.data("unicode-name")
else
$("li[data-emoji='" + emoji + "']").html()
# Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name")
"emoji-#{unicodeName}"
postEmoji: (emoji, callback) ->
$.post @post_emoji_url, { note: {
note: ":" + emoji + ":"
note: ":#{emoji}:"
noteable_type: @noteable_type
noteable_id: @noteable_id
}},(data) ->
......@@ -90,7 +114,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
$(".award [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
......@@ -99,3 +123,44 @@ class @AwardsHandler
normilizeEmojiName: (emoji) ->
@aliases[emoji] || emoji
addEmojiToFrequentlyUsedList: (emoji) ->
frequently_used_emojis = @getFrequentlyUsedEmojis()
frequently_used_emojis.push(emoji)
$.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 })
getFrequentlyUsedEmojis: ->
frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",")
_.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis')
frequently_used_emojis = @getFrequentlyUsedEmojis()
ul = $("<ul>")
for emoji in frequently_used_emojis
do (emoji) ->
$(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
$("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
setupSearch: ->
$("input.emoji-search").keyup (ev) =>
term = $(ev.target).val()
# Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
$(".emoji-menu-content").children().show()
searchEmojis: (term)->
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
......@@ -35,7 +35,7 @@ class @BlobFileDropzone
return
this.on 'sending', (file, xhr, formData) ->
formData.append('new_branch', form.find('.js-new-branch').val())
formData.append('target_branch', form.find('.js-target-branch').val())
formData.append('create_merge_request', form.find('.js-create-merge-request').val())
formData.append('commit_message', form.find('.js-commit-message').val())
return
......
......@@ -49,7 +49,7 @@ class Dispatcher
new DropzoneInput($('.release-form'))
when 'projects:merge_requests:show'
new Diff()
shortcut_handler = new ShortcutsIssuable()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
when "projects:merge_requests:diffs"
new Diff()
......
......@@ -18,7 +18,7 @@ class @IssuableContext
$('.issuable-affix').affix offset:
top: ->
@top = ($('.issuable-affix').offset().top - 60)
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
......
#= require flash
#= require jquery.waitforimages
#= require task_list
......@@ -6,16 +7,47 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
if $("a.btn-close").length
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
initTaskList: ->
$('.issue-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
initIssueBtnEventListeners: ->
issueFailMessage = 'Unable to update this issue at this time.'
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
e.preventDefault()
e.stopImmediatePropagation()
$this = $(this)
isClose = $this.hasClass('btn-close')
$this.prop('disabled', true)
url = $this.attr('href')
$.ajax
type: 'PUT'
url: url,
error: (jqXHR, textStatus, errorThrown) ->
issueStatus = if isClose then 'close' else 'open'
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
if data.saved
$this.addClass('hidden')
if isClose
$('a.btn-reopen').removeClass('hidden')
$('div.status-box-closed').removeClass('hidden')
$('div.status-box-open').addClass('hidden')
else
$('a.btn-close').removeClass('hidden')
$('div.status-box-closed').addClass('hidden')
$('div.status-box-open').removeClass('hidden')
else
new Flash(issueFailMessage, 'alert')
$this.prop('disabled', false)
disableTaskList: ->
$('.issue-details .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.issue-details .js-task-list-container'
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
# TODO (rspeicher): Make the issue description inline-editable like a note so
# that we can re-use its form here
......
......@@ -40,12 +40,12 @@ class @MergeRequest
this.$('.all-commits').removeClass 'hide'
initTaskList: ->
$('.merge-request-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
disableTaskList: ->
$('.merge-request-details .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container'
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
# TODO (rspeicher): Make the merge request description inline-editable like a
# note so that we can re-use its form here
......
......@@ -18,7 +18,7 @@ class @MergeRequestWidget
if data.state == "merged"
urlSuffix = if deleteSourceBranch then '?delete_source=true' else ''
window.location.href = window.location.href + urlSuffix
window.location.href = window.location.pathname + urlSuffix
else if data.merge_error
$('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
else
......
class @NewBranchForm
constructor: (form, availableRefs) ->
@branchNameError = form.find('.js-branch-name-error')
@name = form.find('.js-branch-name')
@ref = form.find('#ref')
@setupAvailableRefs(availableRefs)
@setupRestrictions()
@addBinding()
@init()
addBinding: ->
@name.on 'blur', @validate
init: ->
@name.trigger 'blur' if @name.val().length > 0
setupAvailableRefs: (availableRefs) ->
@ref.autocomplete
source: availableRefs,
minLength: 1
setupRestrictions: ->
startsWith = {
pattern: /^(\/|\.)/g,
prefix: "can't start with",
conjunction: "or"
}
endsWith = {
pattern: /(\/|\.|\.lock)$/g,
prefix: "can't end in",
conjunction: "or"
}
invalid = {
pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g
prefix: "can't contain",
conjunction: ", "
}
single = {
pattern: /^@+$/g
prefix: "can't be",
conjunction: "or"
}
@restrictions = [startsWith, invalid, endsWith, single]
validate: =>
@branchNameError.empty()
unique = (values, value) ->
values.push(value) unless value in values
values
formatter = (values, restriction) ->
formatted = values.map (value) ->
switch
when /\s/.test value then 'spaces'
when /\/{2,}/g.test value then 'consecutive slashes'
else "'#{value}'"
"#{restriction.prefix} #{formatted.join(restriction.conjunction)}"
validator = (errors, restriction) =>
matched = @name.val().match(restriction.pattern)
if matched
errors.concat formatter(matched.reduce(unique, []), restriction)
else
errors
errors = @restrictions.reduce validator, []
if errors.length > 0
errorMessage = $("<span/>").text(errors.join(', '))
@branchNameError.append(errorMessage)
class @NewCommitForm
constructor: (form) ->
@newBranch = form.find('.js-new-branch')
@newBranch = form.find('.js-target-branch')
@originalBranch = form.find('.js-original-branch')
@createMergeRequest = form.find('.js-create-merge-request')
@createMergeRequestContainer = form.find('.js-create-merge-request-container')
......
......@@ -127,7 +127,7 @@ class @Notes
@initTaskList()
if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
awards_handler.addAwardToEmojiBar(note.note)
awards_handler.scrollToAwards()
###
......
class @Project
constructor: ->
# Git protocol switcher
$('.js-protocol-switch').click ->
$('ul.clone-options-dropdown a').click ->
return if $(@).hasClass('active')
......@@ -10,7 +10,8 @@ class @Project
# Add the active class for the clicked button
$(@).toggleClass('active')
url = $(@).data('clone')
url = $("#project_clone").val()
console.log("url",url)
# Update the input field
$('#project_clone').val(url)
......
......@@ -8,17 +8,17 @@ class @ProjectsList
$(".projects-list-filter").keyup ->
terms = $(this).val()
uiBox = $(this).closest('.projects-list-holder')
uiBox = $('div.projects-list-holder')
if terms == "" || terms == undefined
uiBox.find(".projects-list li").show()
uiBox.find("ul.projects-list li").show()
else
uiBox.find(".projects-list li").each (index) ->
name = $(this).find(".filter-title").text()
uiBox.find("ul.projects-list li").each (index) ->
name = $(this).find("span.filter-title").text()
if name.toLowerCase().search(terms.toLowerCase()) == -1
$(this).hide()
else
$(this).show()
uiBox.find(".projects-list li.bottom").hide()
uiBox.find("ul.projects-list li.bottom").hide()
......@@ -7,7 +7,7 @@ class @Shortcuts
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
......@@ -17,8 +17,7 @@ class @Shortcuts
dataType: 'script',
success: (e) ->
if location and location.length > 0
for l in location
$(l).show()
$(l).show() for l in location
else
$('.hidden-shortcut').show()
$('.js-more-help-button').remove()
......@@ -28,3 +27,8 @@ class @Shortcuts
@focusSearch: (e) ->
$('#search').focus()
e.preventDefault()
$(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
class @Star
constructor: ->
$('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) ->
$this = $(this)
$starSpan = $this.find('span')
$starIcon = $this.find('i')
toggleStar = (isStarred) ->
$this.parent().find('span.count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
else
$starSpan.addClass('starred').text 'Unstar'
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
return
toggleStar $starSpan.hasClass('starred')
return
).on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
return
\ No newline at end of file
......@@ -117,5 +117,5 @@ class @UsersSelect
callback(users)
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root?
return url
......@@ -2,8 +2,8 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
*= require jquery.ui.datepicker
*= require jquery.ui.autocomplete
*= require jquery-ui/datepicker
*= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
......@@ -48,4 +48,4 @@
/*
* Styles for JS behaviors.
*/
@import "behaviors.scss";
\ No newline at end of file
@import "behaviors.scss";
......@@ -76,7 +76,7 @@
.cover-block {
text-align: center;
background: #f7f8fa;
background: $background-color;
margin: -$gl-padding;
margin-bottom: 0;
padding: 44px $gl-padding;
......
@mixin btn-default {
@include border-radius(2px);
@include border-radius(3px);
border-width: 1px;
border-style: solid;
text-transform: uppercase;
font-size: 13px;
font-weight: 600;
font-size: 15px;
font-weight: 500;
line-height: 18px;
padding: 11px $gl-padding;
letter-spacing: .4px;
......@@ -18,7 +17,7 @@
@mixin btn-middle {
@include btn-default;
@include border-radius(2px);
@include border-radius(3px);
padding: 11px 24px;
}
......@@ -51,6 +50,10 @@
@include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
}
@mixin btn-blue-medium {
@include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
}
@mixin btn-orange {
@include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
}
......@@ -60,7 +63,7 @@
}
@mixin btn-gray {
@include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236);
@include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, #313236);
}
@mixin btn-white {
......@@ -75,6 +78,10 @@
padding: 5px 10px;
}
&.btn-nr {
padding: 7px 10px;
}
&.btn-xs {
padding: 1px 5px;
}
......@@ -91,11 +98,15 @@
@include btn-gray;
}
&.btn-primary,
&.btn-primary {
@include btn-blue-medium;
}
&.btn-info {
@include btn-blue;
}
&.btn-close,
&.btn-warning {
@include btn-orange;
}
......@@ -110,20 +121,8 @@
float: right;
}
&.btn-close {
color: $gl-danger;
border-color: $gl-danger;
&:hover {
color: #B94A48;
}
}
&.btn-reopen {
color: $gl-success;
border-color: $gl-success;
&:hover {
color: #468847;
}
/* should be same as parent class for now */
}
&.btn-grouped {
......
......@@ -374,7 +374,7 @@ table {
}
}
.center-top-menu {
.center-top-menu, .left-top-menu {
@include nav-menu;
text-align: center;
margin-top: 5px;
......@@ -401,6 +401,16 @@ table {
border-bottom: 1px solid $border-color;
height: 57px;
}
&.wide {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
}
}
.left-top-menu {
text-align: left;
border-bottom: 1px solid #EEE;
}
.center-middle-menu {
......
......@@ -4,8 +4,8 @@
*
*/
.issue-box {
@include border-radius(2px);
.status-box {
@include border-radius(3px);
display: block;
float: left;
......@@ -14,22 +14,22 @@
margin-right: 10px;
font-size: $gl-font-size;
&.issue-box-closed {
&.status-box-closed {
background-color: $gl-danger;
color: #FFF;
}
&.issue-box-merged {
&.status-box-merged {
background-color: $gl-primary;
color: #FFF;
}
&.issue-box-open {
background-color: #019875;
&.status-box-open {
background-color: $green-light;
color: #FFF;
}
&.issue-box-expired {
&.status-box-expired {
background: #cea61b;
color: #FFF;
}
......
......@@ -5,7 +5,7 @@ html {
}
body {
background-color: #EAEBEC !important;
background-color: #F3F3F3 !important;
&.navless {
background-color: white !important;
......
......@@ -143,7 +143,11 @@ ul.controls {
> li {
float: left;
padding-right: 10px;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
.author_link {
display: inline-block;
......
......@@ -87,7 +87,7 @@
.new_note,
.edit_note,
.issuable-description,
.detail-page-description,
.milestone-description,
.wiki-content,
.merge-request-form {
......
......@@ -123,7 +123,6 @@
padding: 0;
margin: 0;
list-style: none;
margin-top: 5px;
height: 56px;
li {
......@@ -131,9 +130,9 @@
a {
padding: 14px;
font-size: 17px;
font-size: 15px;
line-height: 28px;
color: #7f8fa4;
color: #959494;
border-bottom: 2px solid transparent;
&:hover, &:active, &:focus {
......@@ -143,8 +142,8 @@
}
&.active a {
color: #4c4e54;
border-bottom: 2px solid #1cacfc;
color: #616060;
border-bottom: 2px solid #4688f1;
}
.badge {
......
......@@ -81,7 +81,7 @@
display: none;
}
.center-top-menu {
.center-top-menu, .left-top-menu {
li a {
font-size: 14px;
padding: 19px 10px;
......
......@@ -10,8 +10,7 @@
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid #ECEEF1;
border-right: 1px solid #ECEEF1;
border-bottom: 1px solid $border-white-light;
&:target {
background: $hover;
......
$hover: #FFFAF1;
$hover: #faf9f9;
$gl-text-color: #54565B;
$gl-text-green: #4A2;
$gl-text-red: #D12F19;
$gl-text-orange: #D90;
$gl-header-color: #4c4e54;
$gl-header-color: #323232;
$gl-link-color: #333c48;
$md-text-color: #444;
$md-link-color: #3084bb;
......@@ -15,13 +15,14 @@ $sidebar_width: 230px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
$border-color: #dce0e6;
$border-color: #efeff1;
$table-border-color: #eef0f2;
$background-color: #F7F8FA;
$background-color: #faf9f9;
$header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #7f8fa4;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
$gl-padding-top:10px;
$gl-avatar-size: 46px;
/*
......@@ -29,12 +30,12 @@ $gl-avatar-size: 46px;
*/
$white-light: #FFFFFF;
$white-normal: #DCE0E5;
$white-dark: #E4E7ED;
$white-normal: #ededed;
$white-dark: #ededed;
$gray-light: #F0F2F5;
$gray-normal: #DCE0E5;
$gray-dark: #E4E7ED;
$gray-light: #f7f7f7;
$gray-normal: #ededed;
$gray-dark: #ededed;
$green-light: #31AF64;
$green-normal: #2FAA60;
......@@ -44,6 +45,10 @@ $blue-light: #2EA8E5;
$blue-normal: #2D9FD8;
$blue-dark: #2897CE;
$blue-medium-light: #3498CB;
$blue-medium: #2F8EBF;
$blue-medium-dark: #2D86B4;
$orange-light: #FC6443;
$orange-normal: #E75E40;
$orange-dark: #CE5237;
......@@ -52,11 +57,11 @@ $red-light: #F43263;
$red-normal: #E52C5A;
$red-dark: #D22852;
$border-white-light: #E3E7EC;
$border-white-light: #F1F2F4;
$border-white-normal: #D6DAE2;
$border-white-dark: #C6CACF;
$border-gray-light: #DCE0E5;
$border-gray-light: #d1d1d1;
$border-gray-normal: #D6DAE2;
$border-gray-dark: #C6CACF;
......@@ -76,6 +81,8 @@ $border-red-light: #E52C5A;
$border-red-normal: #D22852;
$border-red-dark: #CA264F;
/* header */
$light-grey-header: #faf9f9;
/*
* State colors:
......
......@@ -2,6 +2,12 @@
@include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
margin: 7px 0 0 5px;
}
.award {
@include border-radius(5px);
......@@ -40,6 +46,7 @@
}
.awards-controls {
position: relative;
margin-left: 10px;
float: left;
......@@ -55,32 +62,64 @@
}
}
.awards-menu {
padding: $gl-padding;
min-width: 214px;
.emoji-menu{
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
box-shadow: 0 6px 12px rgba(0,0,0,.175);
.emoji-menu-content {
padding: $gl-padding;
width: 300px;
height: 300px;
overflow-y: scroll;
h5 {
clear: left;
}
> li {
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
@include border-radius(5px);
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
}
img {
margin-bottom: 2px;
input.emoji-search{
background: image-url("icon-search.png") 240px no-repeat;
}
&:hover {
background-color: #ccc;
li {
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
&:hover {
background-color: #ccc;
}
}
}
}
}
.awards-menu{
li {
float: left;
margin: 3px;
}
}
}
.detail-page-header {
margin: -$gl-padding;
padding: 7px $gl-padding;
margin-bottom: 0px;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
line-height: 34px;
.author {
color: #5c5d5e;
}
.identifier {
color: #5c5d5e;
}
}
.detail-page-description {
.title {
margin: 0;
font-size: 23px;
color: #313236;
}
.description {
margin-top: 6px;
p:last-child {
margin-bottom: 0;
}
}
}
This diff is collapsed.
......@@ -18,7 +18,7 @@
&.affix {
position: fixed;
top: 60px;
top: 70px;
margin-right: 35px;
}
}
......@@ -36,33 +36,12 @@
}
.issuable-details {
.issue-title {
margin: 0;
font-size: 23px;
color: #313236;
}
.description {
margin-top: 6px;
p:last-child {
margin-bottom: 0;
}
}
section {
border-right: 1px solid #ECEEF1;
border-right: 1px solid $border-white-light;
> .tab-content {
.issuable-discussion {
margin-right: 1px;
}
.issue-discussion > .gray-content-block,
> .gray-content-block {
margin-top: 0;
border-top: none;
margin-right: -15px;
}
}
}
......@@ -136,21 +115,3 @@
margin-right: 2px;
}
}
.issuable-title {
margin: -$gl-padding;
padding: 7px $gl-padding;
margin-bottom: 0px;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
line-height: 42px;
.author {
color: #5c5d5e;
}
.issuable-id {
color: #5c5d5e;
}
}
......@@ -141,11 +141,6 @@ form.edit-issue {
}
}
.issue-closed-by-widget {
padding: 16px 0;
margin: 0px;
}
.issue-form .select2-container {
width: 250px !important;
}
......@@ -191,7 +191,7 @@
.btn-clipboard {
@extend .pull-right;
margin-right: 18px;
margin-right: 20px;
margin-top: 5px;
position: absolute;
right: 0;
......
......@@ -75,17 +75,15 @@
.common-note-form {
margin: 0;
background: #F7F8FA;
background: #fff;
padding: $gl-padding;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border-right: 1px solid $border-color;
border-top: 1px solid $border-color;
margin-bottom: -$gl-padding;
}
.note-form-actions {
background: #F9F9F9;
background: #fff;
.note-form-option {
margin-top: 8px;
......
......@@ -128,7 +128,7 @@ ul.notes {
}
&:last-child {
border-bottom: none;
border-bottom: 1px solid $border-color;
}
}
}
......
......@@ -91,21 +91,83 @@
}
}
.input-group {
.git-clone-holder {
display: inline-table;
position: relative;
top: 17px;
}
.project-repo-buttons {
margin-top: 12px;
margin-bottom: 0px;
.count-buttons {
display: block;
margin-bottom: 12px;
}
.btn {
@include btn-gray;
text-transform: none;
}
.count-with-arrow {
display: inline-block;
position: relative;
margin-left: 4px;
.arrow {
&:before {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 50%;
left: 0;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: #dce0e5;
}
&:after {
content: '';
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 50%;
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: #FFF;
}
}
.count {
@include btn-gray;
display: inline-block;
background: white;
border-radius: 2px;
border-width: 1px;
border-style: solid;
font-size: 13px;
font-weight: 600;
line-height: 20px;
padding: 11px 16px;
letter-spacing: .4px;
padding: 10px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
white-space: nowrap;
margin: 0 11px 0px 4px;
&:hover {
background: #FFF;
}
}
}
}
......@@ -125,6 +187,13 @@
margin-right: 45px;
}
.clone-options {
display: table-cell;
a.btn {
width: 100%;
}
}
.form-control {
cursor: auto;
@extend .monospace;
......@@ -335,6 +404,38 @@ ul.nav.nav-projects-tabs {
}
}
.top-area {
border-bottom: 1px solid #EEE;
margin: 0 -16px;
padding: 0 $gl-padding;
ul.left-top-menu {
display: inline-block;
width: 50%;
margin-bottom: 0px;
border-bottom: none;
}
.projects-search-form {
width: 50%;
display: inline-block;
float: right;
padding-top: 7px;
text-align: right;
.btn-green {
margin-top: -2px;
margin-left: 10px;
}
}
@media (max-width: $screen-xs-max) {
.projects-search-form {
padding-top: 15px;
}
}
}
.fork-namespaces {
.fork-thumbnail {
text-align: center;
......@@ -412,11 +513,18 @@ pre.light-well {
.projects-search-form {
margin: -$gl-padding;
background-color: #f8fafc;
padding: $gl-padding;
margin-bottom: 0px;
border-top: 1px solid #e7e9ed;
border-bottom: 1px solid #e7e9ed;
input {
display: inline-block;
width: calc(100% - 151px);
}
.btn {
display: inline-block;
width: 135px;
}
}
.git-empty {
......
......@@ -35,3 +35,20 @@
border-color: $gl-warning;
}
}
.ci-status-icon-success {
@extend .cgreen;
}
.ci-status-icon-failed {
@extend .cred;
}
.ci-status-icon-running,
.ci-status-icon-pending {
// These are standard text color
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found,
.ci-status-icon-skipped {
@extend .cgray;
}
......@@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_branch_protection,
:signup_enabled,
:signin_enabled,
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text,
......@@ -65,6 +67,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:user_oauth_applications,
:shared_runners_enabled,
:max_artifacts_size,
:metrics_enabled,
:metrics_host,
:metrics_port,
:metrics_username,
:metrics_password,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
:recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key,
restricted_visibility_levels: [],
import_sources: []
)
......
class Admin::IdentitiesController < Admin::ApplicationController
before_action :user
before_action :identity, except: :index
before_action :identity, except: [:index, :new, :create]
def new
@identity = Identity.new
end
def create
@identity = Identity.new(identity_params)
@identity.user_id = user.id
if @identity.save
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully created.'
else
render :new
end
end
def index
@identities = @user.identities
......
......@@ -10,8 +10,10 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
before_action :default_headers
before_action :add_gon_variables
......@@ -202,12 +204,32 @@ class ApplicationController < ActionController::Base
end
end
def validate_user_service_ticket!
return unless signed_in? && session[:service_tickets]
valid = session[:service_tickets].all? do |provider, ticket|
Gitlab::OAuth::Session.valid?(provider, ticket)
end
unless valid
session[:service_tickets] = nil
sign_out current_user
redirect_to new_user_session_path
end
end
def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
redirect_to new_profile_password_path and return
end
end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path
end
end
def ldap_security_check
if current_user && current_user.requires_ldap_check?
unless Gitlab::LDAP::Access.allowed?(current_user)
......@@ -342,6 +364,23 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
def two_factor_grace_period
current_application_settings.two_factor_grace_period
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def skip_two_factor?
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
......
......@@ -19,8 +19,10 @@ module Ci
@error = e.message
@status = false
rescue
@error = "Undefined error"
@error = 'Undefined error'
@status = false
ensure
render :show
end
end
end
module CreatesCommit
extend ActiveSupport::Concern
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
set_commit_variables
commit_params = @commit_params.merge(
source_project: @project,
source_branch: @ref,
target_branch: @target_branch
)
result = service.new(@tree_edit_project, current_user, commit_params).execute
if result[:status] == :success
flash[:notice] = success_notice || "Your changes have been successfully committed."
if create_merge_request?
success_path = new_merge_request_path
target = different_project? ? "project" : "branch"
flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
end
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html do
if failure_view
render failure_view
else
redirect_to failure_path
end
end
format.json { render json: { message: "failed", filePath: failure_path } }
end
end
end
def authorize_edit_tree!
return if can?(current_user, :push_code, project)
return if current_user && current_user.already_forked?(project)
access_denied!
end
private
def new_merge_request_path
new_namespace_project_merge_request_path(
@mr_source_project.namespace,
@mr_source_project,
merge_request: {
source_project_id: @mr_source_project.id,
target_project_id: @mr_target_project.id,
source_branch: @mr_source_branch,
target_branch: @mr_target_branch
}
)
end
def different_project?
@mr_source_project != @mr_target_project
end
def different_branch?
@mr_source_branch != @mr_target_branch || different_project?
end
def create_merge_request?
params[:create_merge_request].present? && different_branch?
end
def set_commit_variables
@mr_source_branch = @target_branch
if can?(current_user, :push_code, @project)
# Edit file in this project
@tree_edit_project = @project
@mr_source_project = @project
if @project.forked?
# Merge request from this project to fork origin
@mr_target_project = @project.forked_from_project
@mr_target_branch = @mr_target_project.repository.root_ref
else
# Merge request to this project
@mr_target_project = @project
@mr_target_branch = @ref
end
else
# Edit file in fork
@tree_edit_project = current_user.fork_of(@project)
# Merge request from fork to this project
@mr_source_project = @tree_edit_project
@mr_target_project = @project
@mr_target_branch = @mr_target_project.repository.root_ref
end
end
end
module CreatesMergeRequestForCommit
extend ActiveSupport::Concern
def new_merge_request_path
if @project.forked?
target_project = @project.forked_from_project || @project
target_branch = target_project.repository.root_ref
else
target_project = @project
target_branch = @ref
end
new_namespace_project_merge_request_path(
@project.namespace,
@project,
merge_request: {
source_project_id: @project.id,
target_project_id: target_project.id,
source_branch: @new_branch,
target_branch: target_branch
}
)
end
def create_merge_request?
params[:create_merge_request] && @new_branch != @ref
end
end
class Dashboard::SnippetsController < Dashboard::ApplicationController
def index
@snippets = SnippetsFinder.new.execute(current_user,
@snippets = SnippetsFinder.new.execute(
current_user,
filter: :by_user,
user: current_user,
scope: params[:scope]
......
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml]
protect_from_forgery except: [:kerberos, :saml, :cas3]
Gitlab.config.omniauth.providers.each do |provider|
define_method provider['name'] do
......@@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
render 'errors/omniauth_error', layout: "errors", status: 422
end
def cas3
ticket = params['ticket']
if ticket
handle_service_ticket oauth['provider'], ticket
end
handle_omniauth
end
private
def handle_omniauth
......@@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to new_user_session_path
end
def handle_service_ticket provider, ticket
Gitlab::OAuth::Session.create provider, ticket
session[:service_tickets] ||= {}
session[:service_tickets][provider] = ticket
end
def oauth
@oauth ||= request.env['omniauth.auth']
end
......
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
def new
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
current_user.save!
end
unless current_user.otp_grace_period_started_at && two_factor_grace_period
current_user.otp_grace_period_started_at = Time.current
end
current_user.save! if current_user.changed?
if two_factor_grace_period_expired?
flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
end
@qr_code = build_qr_code
......@@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
redirect_to profile_account_path
end
def skip
if two_factor_grace_period_expired?
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
else
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path
end
end
private
def build_qr_code
......
# Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesMergeRequestForCommit
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
......@@ -9,21 +9,21 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:destroy, :create]
before_action :authorize_edit_tree!, only: [:new, :create, :edit, :update, :destroy]
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
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 :after_edit_path, only: [:edit, :update]
def new
commit unless @repository.empty?
end
def create
create_commit(Files::CreateService, success_path: after_create_path,
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)),
failure_view: :new,
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end
......@@ -36,6 +36,14 @@ class Projects::BlobController < Projects::ApplicationController
end
def update
after_edit_path =
if from_merge_request && @target_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
end
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
......@@ -50,15 +58,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
result = Files::DeleteService.new(@project, current_user, @commit_params).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
redirect_to after_destroy_path
else
flash[:alert] = result[:message]
render :show
end
create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
failure_view: :show,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
......@@ -108,74 +111,13 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
def create_commit(service, success_path:, failure_view:, failure_path:)
result = service.new(@project, current_user, @commit_params).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render failure_view }
format.json { render json: { message: "failed", filePath: failure_path } }
end
end
end
def after_create_path
@after_create_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path))
end
end
def after_edit_path
@after_edit_path ||=
if create_merge_request?
new_merge_request_path
elsif from_merge_request && @new_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path))
end
end
def after_destroy_path
@after_destroy_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_tree_path(@project.namespace, @project, @new_branch)
end
end
def from_merge_request
# If blob edit was initiated from merge request page
@from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
end
def sanitized_new_branch_name
sanitize(strip_tags(params[:new_branch]))
end
def editor_variables
@current_branch = @ref
@new_branch =
if params[:new_branch].present?
sanitized_new_branch_name
elsif ::Gitlab::GitAccess.new(current_user, @project).can_push_to_branch?(@ref)
@ref
else
@repository.next_patch_branch
end
@target_branch = params[:target_branch]
@file_path =
if action_name.to_s == 'create'
......@@ -194,8 +136,6 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = {
file_path: @file_path,
current_branch: @current_branch,
target_branch: @new_branch,
commit_message: params[:commit_message],
file_content: params[:content],
file_content_encoding: params[:encoding]
......
......@@ -9,7 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
def show
@repo = @project.repository
@limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
@commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
......
......@@ -10,19 +10,35 @@ class Projects::ForksController < Projects::ApplicationController
def create
namespace = Namespace.find(params[:namespace_key])
@forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
@forked_project = namespace.projects.find_by(path: project.path)
@forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
@forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
if @forked_project.saved? && @forked_project.forked?
if @forked_project.import_in_progress?
redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project)
redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project, continue: continue_params)
else
redirect_to(
namespace_project_path(@forked_project.namespace, @forked_project),
notice: 'Project was successfully forked.'
)
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked."
end
end
else
render :error
end
end
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
end
class Projects::ImportsController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :require_no_repo
before_action :require_no_repo, except: :show
before_action :redirect_if_progress, except: :show
def new
......@@ -24,21 +24,36 @@ class Projects::ImportsController < Projects::ApplicationController
end
def show
unless @project.import_in_progress?
if @project.import_finished?
redirect_to(project_path(@project)) and return
if @project.repository_exists? || @project.import_finished?
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to(new_namespace_project_import_path(@project.namespace,
@project)) and return
redirect_to project_path(@project), notice: "The project was successfully forked."
end
elsif @project.import_failed?
redirect_to new_namespace_project_import_path(@project.namespace, @project)
else
if continue_params && continue_params[:notice_now]
flash.now[:notice] = continue_params[:notice_now]
end
# Render
end
end
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
def require_no_repo
if @project.repository_exists? && !@project.import_in_progress?
redirect_to(namespace_project_path(@project.namespace, @project)) and return
redirect_to(namespace_project_path(@project.namespace, @project))
end
end
......
......@@ -7,7 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request
......@@ -153,11 +153,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_check
if @merge_request.unchecked?
@merge_request.check_if_can_be_merged
end
closes_issues
@merge_request.check_if_can_be_merged if @merge_request.unchecked?
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end
......@@ -178,7 +174,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds] && @merge_request.ci_commit && @merge_request.ci_commit.active?
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
......@@ -299,6 +295,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars
@ci_commit = @merge_request.ci_commit
closes_issues
end
def invalid_mr
......
......@@ -69,7 +69,7 @@ class Projects::NotesController < Projects::ApplicationController
data = {
author: current_user,
is_award: true,
note: note_params[:note].gsub(":", '')
note: note_params[:note].delete(":")
}
note = noteable.notes.find_by(data)
......@@ -139,7 +139,6 @@ class Projects::NotesController < Projects::ApplicationController
discussion_id: note.discussion_id,
html: note_to_html(note),
award: note.is_award,
emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
......
......@@ -21,7 +21,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
if protected_branch &&
protected_branch.update_attributes(
developers_can_push: params[:developers_can_push]
developers_can_push: params[:developers_can_push]
)
respond_to do |format|
......
class Projects::ServicesController < Projects::ApplicationController
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain,
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :drone_url, :build_type,
......@@ -10,7 +10,8 @@ class Projects::ServicesController < Projects::ApplicationController
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification]
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
......
# Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
include CreatesMergeRequestForCommit
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:create_dir]
before_action :authorize_edit_tree!, only: [:create_dir]
def show
return render_404 unless @repository.commit(@ref)
......@@ -34,44 +34,20 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
begin
result = Files::CreateDirService.new(@project, current_user, @commit_params).execute
message = result[:message]
rescue => e
message = e.to_s
end
if result && result[:status] == :success
flash[:notice] = "The directory has been successfully created"
respond_to do |format|
format.html { redirect_to after_create_dir_path }
end
else
flash[:alert] = message
respond_to do |format|
format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) }
end
end
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
end
private
def assign_dir_vars
@new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
@target_branch = params[:target_branch]
@dir_name = File.join(@path, params[:dir_name])
@commit_params = {
file_path: @dir_name,
current_branch: @ref,
target_branch: @new_branch,
commit_message: params[:commit_message],
}
end
def after_create_dir_path
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name))
end
end
end
......@@ -171,14 +171,14 @@ class ProjectsController < ApplicationController
@project.reload
render json: {
html: view_to_html_string("projects/buttons/_star")
star_count: @project.star_count
}
end
def markdown_preview
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
render json: {
......
class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled?
include Recaptcha::Verify
def new
redirect_to(new_user_session_path)
end
def create
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
flash.delete :recaptcha_error
render action: 'new'
end
end
def destroy
DeleteUserService.new(current_user).execute(current_user)
......@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
def sign_up_params
params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
end
def resource_name
:user
end
def resource
@resource ||= User.new(sign_up_params)
end
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
end
class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Recaptcha::ClientHelper
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
def new
if Gitlab.config.ldap.enabled
......@@ -40,7 +42,7 @@ class SessionsController < Devise::SessionsController
User.find(session[:otp_user_id])
end
end
def store_redirect_path
redirect_path =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
......@@ -87,14 +89,14 @@ class SessionsController < Devise::SessionsController
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present?
# Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
# registered or no alert at all. In case of another alert (such as a blocked user), it is safer
# Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
# registered or no alert at all. In case of another alert (such as a blocked user), it is safer
# to do nothing to prevent redirection loops with certain Omniauth providers.
return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated')
# Prevent alert from popping up on the first page shown after authentication.
flash[:alert] = nil
flash[:alert] = nil
redirect_to user_omniauth_authorize_path(provider.to_sym)
end
......@@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController
AuditEventService.new(user, user, options).
for_authentication.security_event
end
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
end
......@@ -61,7 +61,7 @@ module ApplicationHelper
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555"
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
......@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
user = User.find_by(email: user_or_email)
user = User.find_by(email: user_or_email.downcase)
end
if user
......@@ -204,12 +204,16 @@ module ApplicationHelper
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago",
class: "#{html_class} js-timeago js-timeago-pending",
datetime: time.getutc.iso8601,
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js
unless skip_js
element << javascript_tag(
"$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
)
end
element
end
......
......@@ -50,5 +50,17 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s)
end
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
def two_factor_grace_period_expired?
current_user.otp_grace_period_started_at &&
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
extend self
end
......@@ -22,32 +22,90 @@ module BlobHelper
%w(credits changelog news copying copyright license authors)
end
def edit_blob_link(project, ref, path, options = {})
blob =
begin
project.repository.blob_at(ref, path)
rescue
nil
end
return unless blob && blob.text? && blob_editable?(blob)
text = 'Edit'
after = options[:after] || ''
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil
return unless blob && blob_text_viewable?(blob)
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
cls = 'btn btn-small'
link_to(text,
namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts),
class: cls
) + after.html_safe
edit_path = namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts)
if !on_top_of_branch?
button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob)
link_to "Edit", edit_path, class: 'btn btn-small'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
continue: continue_params)
link_to "Edit", fork_path, class: 'btn btn-small', method: :post
end
end
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil
return unless blob
if !on_top_of_branch?
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_edit_blob?(blob)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
continue: continue_params)
link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
end
end
def replace_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
project,
ref,
path,
label: "Replace",
action: "replace",
btn_class: "default",
modal_type: "upload"
)
end
def delete_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
project,
ref,
path,
label: "Delete",
action: "delete",
btn_class: "remove",
modal_type: "remove"
)
end
def blob_editable?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && allowed_tree_edit?(project, ref)
def can_edit_blob?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && can_edit_tree?(project, ref)
end
def leave_edit_message
......@@ -70,7 +128,7 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
def blob_viewable?(blob)
def blob_text_viewable?(blob)
blob && blob.text? && !blob.lfs_pointer?
end
......
......@@ -10,8 +10,8 @@ module ButtonHelper
# # => "<button class='...' data-clipboard-text='Foo'>...</button>"
#
# # Define the target element
# clipboard_button(clipboard_target: "#foo")
# # => "<button class='...' data-clipboard-target='#foo'>...</button>"
# clipboard_button(clipboard_target: "div#foo")
# # => "<button class='...' data-clipboard-target='div#foo'>...</button>"
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
......
......@@ -12,19 +12,6 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status)
end
def ci_status_color(ci_commit)
case ci_commit.status
when 'success'
'green'
when 'failed'
'red'
when 'running', 'pending'
'yellow'
else
'gray'
end
end
def ci_status_with_icon(status)
content_tag :span, class: "ci-status ci-#{status}" do
ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
......@@ -56,12 +43,11 @@ module CiStatusHelper
end
def render_ci_status(ci_commit)
link_to ci_status_path(ci_commit),
class: "ci-status-link c#{ci_status_color(ci_commit)}",
link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}",
data: { toggle: 'tooltip', placement: 'left' } do
ci_status_icon(ci_commit)
end
data: { toggle: 'tooltip', placement: 'left' }
end
def no_runners_for_project?(project)
......
module ExternalWikiHelper
def get_project_wiki_path(project)
external_wiki_service = project.services.
select { |service| service.to_param == 'external_wiki' }.first
find { |service| service.to_param == 'external_wiki' }
if external_wiki_service.present? && external_wiki_service.active?
external_wiki_service.properties['external_wiki_url']
else
......
......@@ -65,7 +65,8 @@ module GitlabMarkdownHelper
end
def asciidoc(text)
Gitlab::Asciidoc.render(text,
Gitlab::Asciidoc.render(
text,
project: @project,
current_user: (current_user if defined?(current_user)),
......
......@@ -57,18 +57,22 @@ module IssuesHelper
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
def issue_box_class(item)
def status_box_class(item)
if item.respond_to?(:expired?) && item.expired?
'issue-box-expired'
'status-box-expired'
elsif item.respond_to?(:merged?) && item.merged?
'issue-box-merged'
'status-box-merged'
elsif item.closed?
'issue-box-closed'
'status-box-closed'
else
'issue-box-open'
'status-box-open'
end
end
def issue_button_visibility(issue, closed)
return 'hidden' if issue.closed? == closed
end
def issue_to_atom(xml, issue)
xml.entry do
xml.id namespace_project_issue_url(issue.project.namespace,
......@@ -94,11 +98,14 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
def url_to_emoji(name)
emoji_path = ::AwardEmoji.path_to_emoji_image(name)
url_to_image(emoji_path)
rescue StandardError
""
def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name)
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
"data-emoji" => name,
"data-aliases" => aliases.join(" "),
"data-unicode-name" => unicode
end
def emoji_author_list(notes, current_user)
......@@ -109,10 +116,6 @@ module IssuesHelper
list.join(", ")
end
def emoji_list
::AwardEmoji::EMOJI_LIST
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
"active"
......@@ -121,6 +124,18 @@ module IssuesHelper
end
end
def awards_sort(awards)
awards.sort_by do |award, notes|
if award == "thumbsup"
0
elsif award == "thumbsdown"
1
else
2
end
end.to_h
end
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
......@@ -27,7 +27,16 @@ module MergeRequestsHelper
end
def ci_build_details_path(merge_request)
merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
return nil unless build_url
parsed_url = URI.parse(build_url)
unless parsed_url.userinfo.blank?
parsed_url.userinfo = ''
end
parsed_url.to_s
end
def merge_path_description(merge_request, separator)
......
......@@ -8,6 +8,80 @@ module PageLayoutHelper
@page_title.join(" \u00b7 ")
end
# Define or get a description for the current page
#
# description - String (default: nil)
#
# If this helper is called multiple times with an argument, only the last
# description will be returned when called without an argument. Descriptions
# have newlines replaced with spaces and all HTML tags are sanitized.
#
# Examples:
#
# page_description # => "GitLab Community Edition"
# page_description("Foo")
# page_description # => "Foo"
#
# page_description("<b>Bar</b>\nBaz")
# page_description # => "Bar Baz"
#
# Returns an HTML-safe String.
def page_description(description = nil)
@page_description ||= page_description_default
if description.present?
@page_description = description.squish
else
sanitize(@page_description, tags: []).truncate_words(30)
end
end
# Default value for page_description when one hasn't been defined manually by
# a view
def page_description_default
if @project
@project.description || brand_title
else
brand_title
end
end
def page_image
default = image_url('gitlab_logo.png')
if @project
@project.avatar_url || default
elsif @user
avatar_icon(@user)
else
default
end
end
# Define or get attributes to be used as Twitter card metadata
#
# map - Hash of label => data pairs. Keys become labels, values become data
#
# Raises ArgumentError if given more than two attributes
def page_card_attributes(map = {})
raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
@page_card_attributes ||= {}
@page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
@page_card_attributes
end
def page_card_meta_tags
tags = ''
page_card_attributes.each_with_index do |pair, i|
tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
end
tags.html_safe
end
def header_title(title = nil, title_url = nil)
if title
@header_title = title
......
......@@ -105,6 +105,14 @@ module ProjectsHelper
end
end
def user_max_access_in_project(user_id, project)
level = project.team.max_member_access(user_id)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
private
def get_project_nav_tabs(project, current_user)
......@@ -277,14 +285,6 @@ module ProjectsHelper
end
end
def user_max_access_in_project(user, project)
level = project.team.max_member_access(user)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
......@@ -330,10 +330,9 @@ module ProjectsHelper
def filename_path(project, filename)
if project && blob = project.repository.send(filename)
namespace_project_blob_path(
project.namespace,
project,
tree_join(project.default_branch,
blob.name)
project.namespace,
project,
tree_join(project.default_branch, blob.name)
)
end
end
......
......@@ -50,24 +50,49 @@ module TreeHelper
project.repository.branch_names.include?(ref)
end
def allowed_tree_edit?(project = nil, ref = nil)
def can_edit_tree?(project = nil, ref = nil)
project ||= @project
ref ||= @ref
return false unless on_top_of_branch?(project, ref)
can?(current_user, :push_code, project)
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
end
def tree_edit_branch(project = @project, ref = @ref)
if allowed_tree_edit?(project, ref)
if can_push_branch?(project, ref)
ref
else
project.repository.next_patch_branch
end
return unless can_edit_tree?(project, ref)
if can_push_branch?(project, ref)
ref
else
project = tree_edit_project(project)
project.repository.next_patch_branch
end
end
def tree_edit_project(project = @project)
if can?(current_user, :push_code, project)
project
elsif current_user && current_user.already_forked?(project)
current_user.fork_of(project)
end
end
def edit_in_new_fork_notice_now
"You're not allowed to make changes to this project directly." +
" A fork of this project is being created that you can make changes in, so you can submit a merge request."
end
def edit_in_new_fork_notice
"You're not allowed to make changes to this project directly." +
" A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started."
end
def tree_breadcrumbs(tree, max_links = 2)
if @path.present?
part_path = ""
......@@ -79,7 +104,7 @@ module TreeHelper
part_path = File.join(part_path, part) unless part_path.empty?
part_path = part if part_path.empty?
next unless parts.last(2).include?(part) if parts.count > max_links
next if parts.count > max_links && !parts.last(2).include?(part)
yield(part, tree_join(@ref, part_path))
end
end
......
......@@ -69,7 +69,6 @@ module VisibilityLevelHelper
def skip_level?(form_model, level)
form_model.is_a?(Project) &&
form_model.forked? &&
!Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level)
!form_model.visibility_level_allowed?(level)
end
end
......@@ -17,7 +17,7 @@ class Notify < BaseMailer
subject: subject,
body: body.html_safe,
content_type: 'text/html'
)
)
end
# Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com",
......
......@@ -132,14 +132,14 @@ class Ability
end
def public_project_rules
project_guest_rules + [
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project
]
end
def project_guest_rules
[
@project_guest_rules ||= [
:read_project,
:read_wiki,
:read_issue,
......@@ -157,7 +157,7 @@ class Ability
end
def project_report_rules
project_guest_rules + [
@project_report_rules ||= project_guest_rules + [
:create_commit_status,
:read_commit_statuses,
:download_code,
......@@ -170,7 +170,7 @@ class Ability
end
def project_dev_rules
project_report_rules + [
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
:create_merge_request,
:create_wiki,
......@@ -181,7 +181,7 @@ class Ability
end
def project_archived_rules
[
@project_archived_rules ||= [
:create_merge_request,
:push_code,
:push_code_to_protected_branches,
......@@ -191,7 +191,7 @@ class Ability
end
def project_master_rules
project_dev_rules + [
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
:update_merge_request,
......@@ -206,7 +206,7 @@ class Ability
end
def project_admin_rules
project_master_rules + [
@project_admin_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
......@@ -332,7 +332,7 @@ class Ability
end
if snippet.public? || snippet.internal?
rules << :read_personal_snippet
rules << :read_personal_snippet
end
rules
......
......@@ -2,32 +2,34 @@
#
# Table name: application_settings
#
# id :integer not null, primary key
# default_projects_limit :integer
# signup_enabled :boolean
# signin_enabled :boolean
# gravatar_enabled :boolean
# sign_in_text :text
# created_at :datetime
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
# help_page_text :text
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
# id :integer not null, primary key
# default_projects_limit :integer
# signup_enabled :boolean
# signin_enabled :boolean
# gravatar_enabled :boolean
# sign_in_text :text
# created_at :datetime
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
# help_page_text :text
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
# require_two_factor_authentication :boolean default(TRUE)
# two_factor_grace_period :integer default(48)
#
class ApplicationSetting < ActiveRecord::Base
......@@ -42,21 +44,32 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :restricted_signup_domains_raw
validates :session_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :home_page_url,
allow_blank: true,
url: true,
if: :home_page_url_column_exist
allow_blank: true,
url: true,
if: :home_page_url_column_exist
validates :after_sign_out_path,
allow_blank: true,
url: true
allow_blank: true,
url: true
validates :admin_notification_email,
allow_blank: true,
email: true
allow_blank: true,
email: true
validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 }
validates :recaptcha_site_key,
presence: true,
if: :recaptcha_enabled
validates :recaptcha_private_key,
presence: true,
if: :recaptcha_enabled
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
......@@ -112,6 +125,8 @@ class ApplicationSetting < ActiveRecord::Base
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48
)
end
......@@ -126,12 +141,16 @@ class ApplicationSetting < ActiveRecord::Base
def restricted_signup_domains_raw=(values)
self.restricted_signup_domains = []
self.restricted_signup_domains = values.split(
/\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
/x)
/\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
/x)
self.restricted_signup_domains.reject! { |d| d.empty? }
end
def runners_registration_token
ensure_runners_registration_token!
end
end
......@@ -135,6 +135,16 @@ module Ci
predefined_variables + yaml_variables + project_variables + trigger_variables
end
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: commit.gl_project_id)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
merge_request.commits.any? { |ci| ci.id == commit.sha }
end
end
def project
commit.project
end
......@@ -170,7 +180,8 @@ module Ci
def extract_coverage(text, regex)
begin
matches = text.gsub(Regexp.new(regex)).to_a.last
matches = text.scan(Regexp.new(regex)).last
matches = matches.last if matches.kind_of?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
......@@ -183,8 +194,11 @@ module Ci
end
def raw_trace
if File.exist?(path_to_trace)
if File.file?(path_to_trace)
File.read(path_to_trace)
elsif project.ci_id && File.file?(old_path_to_trace)
# Temporary fix for build trace data integrity
File.read(old_path_to_trace)
else
# backward compatibility
read_attribute :trace
......@@ -201,8 +215,8 @@ module Ci
end
def trace=(trace)
unless Dir.exists? dir_to_trace
FileUtils.mkdir_p dir_to_trace
unless Dir.exists?(dir_to_trace)
FileUtils.mkdir_p(dir_to_trace)
end
File.write(path_to_trace, trace)
......@@ -220,6 +234,55 @@ module Ci
"#{dir_to_trace}/#{id}.log"
end
##
# Deprecated
#
# This is a hotfix for CI build data integrity, see #4246
# Should be removed in 8.4, after CI files migration has been done.
#
def old_dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
created_at.utc.strftime("%Y_%m"),
project.ci_id.to_s
)
end
##
# Deprecated
#
# This is a hotfix for CI build data integrity, see #4246
# Should be removed in 8.4, after CI files migration has been done.
#
def old_path_to_trace
"#{old_dir_to_trace}/#{id}.log"
end
##
# Deprecated
#
# This contains a hotfix for CI build data integrity, see #4246
#
# This method is used by `ArtifactUploader` to create a store_dir.
# Warning: Uploader uses it after AND before file has been stored.
#
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
old = File.join(created_at.utc.strftime('%Y_%m'),
project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
return old if project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
project.id.to_s,
id.to_s
)
end
def token
project.runners_token
end
......
......@@ -218,16 +218,6 @@ module Ci
update!(committed_at: DateTime.now)
end
##
# This method checks if build status should be displayed.
#
# Build status should be available only if builds are enabled
# on project level and `.gitlab-ci.yml` file is present.
#
def show_build_status?
project.builds_enabled? && ci_yaml_file
end
private
def save_yaml_error(error)
......
......@@ -95,14 +95,12 @@ module Issuable
opened? || reopened?
end
# Deprecated. Still exists to preserve API compatibility.
def downvotes
0
notes.awards.where(note: "thumbsdown").count
end
# Deprecated. Still exists to preserve API compatibility.
def upvotes
0
notes.awards.where(note: "thumbsup").count
end
def subscribed?(user)
......@@ -161,6 +159,14 @@ module Issuable
self.class.to_s.underscore
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
def notes_with_associations
notes.includes(:author, :project)
end
......
......@@ -44,14 +44,14 @@ module Mentionable
end
def all_references(current_user = self.author, text = nil)
ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author)
if text
ext.analyze(text)
else
self.class.mentionable_attrs.each do |attr, options|
text = send(attr)
options[:cache_key] = [self, attr] if options.delete(:cache)
options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted?
ext.analyze(text, options)
end
end
......
......@@ -37,7 +37,7 @@ module Participable
# Be aware that this method makes a lot of sql queries.
# Save result into variable if you are going to reuse it inside same request
def participants(current_user = self.author, load_lazy_references: true)
def participants(current_user = self.author)
participants =
Gitlab::ReferenceExtractor.lazily do
self.class.participant_attrs.flat_map do |attr|
......
......@@ -13,20 +13,21 @@ module TokenAuthenticatable
@token_fields << token_field
define_singleton_method("find_by_#{token_field}") do |token|
where(token_field => token).first if token
find_by(token_field => token) if token
end
define_method("ensure_#{token_field}") do
current_token = read_attribute(token_field)
if current_token.blank?
write_attribute(token_field, generate_token_for(token_field))
else
current_token
end
current_token.blank? ? write_new_token(token_field) : current_token
end
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank?
read_attribute(token_field)
end
define_method("reset_#{token_field}!") do
write_attribute(token_field, generate_token_for(token_field))
write_new_token(token_field)
save!
end
end
......@@ -34,10 +35,15 @@ module TokenAuthenticatable
private
def generate_token_for(token_field)
def write_new_token(token_field)
new_token = generate_token(token_field)
write_attribute(token_field, new_token)
end
def generate_token(token_field)
loop do
token = Devise.friendly_token
break token unless self.class.unscoped.where(token_field => token).first
break token unless self.class.unscoped.find_by(token_field => token)
end
end
end
......@@ -16,7 +16,7 @@ class GlobalMilestone
end
def safe_title
@title.to_slug.to_s
@title.to_slug.normalize.to_s
end
def expired?
......
......@@ -12,6 +12,7 @@
class Identity < ActiveRecord::Base
include Sortable
include CaseSensitivity
belongs_to :user
validates :provider, presence: true
......
......@@ -86,7 +86,7 @@ class Issue < ActiveRecord::Base
def referenced_merge_requests
Gitlab::ReferenceExtractor.lazily do
[self, *notes].flat_map do |note|
note.all_references(load_lazy_references: false).merge_requests
note.all_references.merge_requests
end
end.sort_by(&:iid)
end
......
class JiraIssue < ExternalIssue
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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