Commit fe125f8d authored by Fatih Acet's avatar Fatih Acet

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into category-search-dropdown

# Conflicts:
#	app/assets/javascripts/lib/common_utils.js.coffee
parents 6f8626de 5d16d50c
......@@ -92,9 +92,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun"
# retry failed tests 3 times
- retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
- knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
paths:
- knapsack/
......
......@@ -349,7 +349,7 @@ Style/MultilineArrayBraceLayout:
# Avoid multi-line chains of blocks.
Style/MultilineBlockChain:
Enabled: false
Enabled: true
# Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout:
......
......@@ -2,12 +2,15 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations
......@@ -19,11 +22,18 @@ v 8.9.0 (unreleased)
- Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies
- Implement a fair usage of shared runners
- Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects
- Add a metric for the number of new Redis connections created by a transaction
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages
- Added shortcut 'y' for copying a files content hash URL #14470
- Fix groups API to list only user's accessible projects
- Add Environments and Deployments
- Redesign account and email confirmation emails
- Don't fail builds for projects that are deleted
- Support Docker Registry manifest v1
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0
......@@ -34,14 +44,20 @@ v 8.9.0 (unreleased)
- Add DB index on users.state
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Fix race condition on merge when build succeeds
- Links from a wiki page to other wiki pages should be rewritten as expected
- Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
- Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393
- Fix issues filter when ordering by milestone
- Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
- Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
- TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
- Toggle whitespace button now available for compare branches diffs #17881
- Pipelines can be canceled only when there are running builds
- Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page
......@@ -53,6 +69,7 @@ v 8.9.0 (unreleased)
- Use Knapsack only in CI environment
- Cache project build count in sidebar nav
- Add milestone expire date to the right sidebar
- Manually mark a issue or merge request as a todo
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
- Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects
......@@ -66,24 +83,45 @@ v 8.9.0 (unreleased)
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Improve issuables APIs performance when accessing notes !4471
- External links now open in a new tab
- Prevent default actions of disabled buttons and links
- Markdown editor now correctly resets the input value on edit cancellation !4175
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions
- Improved UX of date pickers on issue & milestone forms
- Cache on the database if a project has an active external issue tracker.
- Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
- All classes in the Banzai::ReferenceParser namespace are now instrumented
v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
- Import GitHub repositories respecting the API rate limit
- Fix importer for GitHub comments on diff
- Disable Webhooks before proceeding with the GitHub import
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace
- Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects
- Measure CPU time for instrumented methods
- Instrument private methods and private instance methods by default instead just public methods
- Only show notes through JSON on confidential issues that the user has access to
- Updated the allocations Gem to version 1.0.5
- The background sampler now ignores classes without names
- Update design for `Close` buttons
- New custom icons for navigation
- Horizontally scrolling navigation on project, group, and profile settings pages
- Hide global side navigation by default
- Fix project Star/Unstar project button tooltip
- Remove tanuki logo from side navigation; center on top nav
- Include user relationships when retrieving award_emoji
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
- Set inverse_of for Project/Service association to reduce the number of queries
v 8.8.5
- Import GitHub repositories respecting the API rate limit !4166
- Fix todos page throwing errors when you have a project pending deletion !4300
- Disable Webhooks before proceeding with the GitHub import !4470
- Fix importer for GitHub comments on diff !4488
- Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user !4498
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace !4541
- Prevent unauthorized access for projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493
- Added descriptions to notification settings dropdown
- Due date can be removed from milestones
v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
......@@ -199,6 +237,9 @@ v 8.8.0
v 8.7.7
- Fix import by `Any Git URL` broken if the URL contains a space
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
v 8.7.6
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
......@@ -361,6 +402,11 @@ v 8.7.0
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
v 8.6.9
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
v 8.6.8
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
......@@ -515,6 +561,10 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
v 8.5.13
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.5.12
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
......@@ -676,6 +726,10 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
v 8.4.11
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.4.10
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
......@@ -812,6 +866,10 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
v 8.3.10
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.3.9
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
......@@ -930,6 +988,10 @@ v 8.3.0
- Expose Git's version in the admin area
- Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
v 8.2.6
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.2.5
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
......
......@@ -210,6 +210,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
# Parse duration
gem 'chronic_duration', '~> 0.10.6'
gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
......@@ -224,7 +227,6 @@ gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
......@@ -245,7 +247,7 @@ end
group :development do
gem "foreman"
gem 'brakeman', '~> 3.2.0', require: false
gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'quiet_assets', '~> 1.0.2'
......
......@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.4)
allocations (1.0.5)
arel (6.0.3)
asana (0.4.0)
faraday (~> 0.9)
......@@ -97,16 +97,7 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.2.1)
erubis (~> 2.6)
haml (>= 3.0, < 5.0)
highline (>= 1.6.20, < 2.0)
ruby2ruby (~> 2.3.0)
ruby_parser (~> 3.8.1)
safe_yaml (>= 1.0)
sass (~> 3.0)
slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
brakeman (3.3.2)
browser (2.0.3)
builder (3.2.2)
bullet (5.0.0)
......@@ -133,6 +124,8 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.3)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
......@@ -284,7 +277,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.1.0)
gitlab_git (10.1.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -338,7 +331,6 @@ GEM
hashie (3.4.3)
health_check (1.5.1)
rails (>= 2.3.0)
highline (1.7.8)
hipchat (1.5.2)
httparty
mimemagic
......@@ -408,7 +400,7 @@ GEM
mime-types (>= 1.16, < 4)
mail_room (0.7.0)
method_source (0.8.2)
mime-types (2.99.1)
mime-types (2.99.2)
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
......@@ -424,6 +416,7 @@ GEM
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
numerizer (0.1.1)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
......@@ -563,7 +556,6 @@ GEM
rainbow (2.1.0)
raindrops (0.15.0)
rake (10.5.0)
raphael-rails (2.1.2)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
......@@ -642,10 +634,7 @@ GEM
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.3.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
ruby_parser (3.8.1)
ruby_parser (3.8.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -655,7 +644,7 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.21)
sass (3.4.22)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
......@@ -704,9 +693,6 @@ GEM
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
......@@ -747,10 +733,8 @@ GEM
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.7.6)
term-ansicolor (1.3.2)
tins (~> 1.0)
terminal-table (1.5.2)
test_after_commit (0.4.2)
activerecord (>= 3.2)
thin (1.6.4)
......@@ -759,7 +743,7 @@ GEM
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.2)
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
......@@ -848,7 +832,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0)
brakeman (~> 3.3.0)
browser (~> 2.0.3)
bullet
bundler-audit
......@@ -857,6 +841,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
......@@ -952,7 +937,6 @@ DEPENDENCIES
rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
recaptcha (~> 3.0)
......
......@@ -42,10 +42,10 @@ class @LabelManager
$from = @prioritizedLabels
if $from.find('li').length is 1
$from.find('.empty-message').show()
$from.find('.empty-message').removeClass('hidden')
if not $target.find('li').length
$target.find('.empty-message').hide()
$target.find('.empty-message').addClass('hidden')
$label.detach().appendTo($target)
......@@ -54,6 +54,9 @@ class @LabelManager
if action is 'remove'
xhr = $.ajax url: url, type: 'DELETE'
# Restore empty message
$from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
else
xhr = @savePrioritySort($label, action)
......
......@@ -32,10 +32,6 @@
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2
#= require raphael
#= require g.raphael
#= require g.bar
#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
#= require underscore
......@@ -125,9 +121,10 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize()
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
$(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
$(".js-select-on-focus").on "focusin", ->
......@@ -257,3 +254,31 @@ $ ->
gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize()
new Aside()
# Sidenav pinning
if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
$.cookie('pin_nav', 'false')
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
.removeClass('page-sidebar-pinned')
$('.navbar-fixed-top').removeClass('header-pinned-nav')
$(document)
.off 'click', '.js-nav-pin'
.on 'click', '.js-nav-pin', (e) ->
e.preventDefault()
$(this).toggleClass 'is-active'
if $.cookie('pin_nav') is 'true'
$.cookie 'pin_nav', 'false'
$('.page-with-sidebar')
.removeClass('page-sidebar-pinned')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.removeClass('header-pinned-nav')
.toggleClass('header-collapsed header-expanded')
else
$.cookie 'pin_nav', 'true'
$('.page-with-sidebar').addClass('page-sidebar-pinned')
$('.navbar-fixed-top').addClass('header-pinned-nav')
......@@ -40,7 +40,7 @@ class @AwardsHandler
$menu = $ '.emoji-menu'
if $addBtn.hasClass 'js-note-emoji'
$addBtn.parents('.note').find('.js-awards-block').addClass 'current'
$addBtn.closest('.note').find('.js-awards-block').addClass 'current'
else
$addBtn.closest('.js-awards-block').addClass 'current'
......
......@@ -17,6 +17,8 @@ class @CiBuild
.off 'resize.build'
.on 'resize.build', @hideSidebar
@updateArtifactRemoveDate()
if $('#build-trace').length
@getInitialBuildTrace()
@initScrollButtonAffix()
......@@ -103,3 +105,10 @@ class @CiBuild
$('.js-build-sidebar')
.removeClass 'right-sidebar-collapsed'
.addClass 'right-sidebar-expanded'
updateArtifactRemoveDate: ->
$date = $('.js-artifacts-remove')
if $date.length
date = $date.text()
$date.text $.timefor(new Date(date), ' ')
......@@ -29,6 +29,7 @@ class Dispatcher
new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DueDateSelect()
new GLForm($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
......@@ -53,9 +54,13 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
new MergedButtons()
when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
new MergedButtons()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
new MergedButtons()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
Issuable.init()
......@@ -68,9 +73,7 @@ class Dispatcher
new Diff()
new ZenMode()
shortcut_handler = new ShortcutsNavigation()
when 'projects:commits:show'
shortcut_handler = new ShortcutsNavigation()
when 'projects:activity'
when 'projects:commits:show', 'projects:activity'
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
......@@ -96,6 +99,7 @@ class Dispatcher
when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
new ShortcutsBlob true
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
when 'projects:labels:index'
......@@ -129,15 +133,11 @@ class Dispatcher
new Project()
new ProjectAvatar()
switch path[1]
when 'compare'
shortcut_handler = new ShortcutsNavigation()
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
when 'new'
when 'new', 'show'
new ProjectNew()
when 'show'
new ProjectShow()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
......@@ -146,9 +146,9 @@ class Dispatcher
when 'snippets'
shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show'
when 'labels', 'graphs'
shortcut_handler = new ShortcutsNavigation()
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
'milestones', 'project_members', 'deploy_keys', 'builds', \
'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one
......
class @DueDateSelect
constructor: ->
# Milestone edit/new form
$datePicker = $('.datepicker')
if $datePicker.length
$dueDate = $('#milestone_due_date')
$datePicker.datepicker
dateFormat: 'yy-mm-dd'
onSelect: (dateText, inst) ->
$dueDate.val(dateText)
.datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()))
$('.js-clear-due-date').on 'click', (e) ->
e.preventDefault()
$.datepicker._clearDate($datePicker)
# Issuable sidebar
$loading = $('.js-issuable-update .due_date')
.find('.block-loading')
.hide()
......@@ -32,7 +48,7 @@ class @DueDateSelect
date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date
else
mediumDate = 'None'
mediumDate = 'No due date'
data = {}
data[abilityName] = {}
......@@ -50,7 +66,8 @@ class @DueDateSelect
$selectbox.hide()
$value.css('display', '')
$valueContent.html(mediumDate)
cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
$valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
$sidebarValue.html(mediumDate)
if value isnt ''
......
......@@ -56,13 +56,6 @@ issuable_created = false
Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label')
toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0
$filteredLabels.removeClass('hidden')
else
$filteredLabels.addClass('hidden')
filterResults: (form) =>
formData = form.serialize()
......@@ -71,58 +64,16 @@ issuable_created = false
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: 'GET'
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issuable.reload()
Issuable.updateStateFilters()
$filteredLabels = $('.filtered-labels')
if typeof Issuable.labelRow is 'function'
$filteredLabels.html(Issuable.labelRow(data))
Issuable.toggleLabelFilters()
dataType: "json"
reload: ->
if Issuable.created
Issuable.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
Turbolinks.visit(issuesUrl);
initChecks: ->
$('.check_all_issues').on 'click', ->
$('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked)
Issuable.checkChanged()
)
$('.selected_issue').on 'change', Issuable.checkChanged
updateStateFilters: ->
stateFilters = $('.issues-state-filters, .dropdown-menu-sort')
newParams = {}
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
labelNameValues = gl.utils.getParameterValues('label_name[]')
if labelNameValues
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
else
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
$(this).attr 'href', newUrl
$('.selected_issue').off('change').on('change', Issuable.checkChanged)
checkChanged: ->
checked_issues = $('.selected_issue:checked')
......
......@@ -102,6 +102,10 @@ class @IssuableForm
return {
results: data
}
data: (query) ->
{
search: query
}
formatResult: (project) ->
project.name_with_namespace
formatSelection: (project) ->
......
......@@ -9,6 +9,9 @@ class @IssuableBulkActions
@bindEvents()
# Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
getElement: (selector) ->
@container.find selector
......@@ -97,13 +100,22 @@ class @IssuableBulkActions
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
labelIds.push $(label).val() if label
labelIds.push parseInt($(label).val()) if label
labelIds
###*
* Just an alias of @getUnmarkedIndeterminedLabels
* @return {Array} Array of labels
* Returns Label IDs that will be removed from issue selection
* @return {Array} Array of labels IDs
###
getLabelsToRemove: ->
@getUnmarkedIndeterminedLabels()
result = []
indeterminatedLabels = @getUnmarkedIndeterminedLabels()
labelsToApply = @getLabelsToApply()
indeterminatedLabels.map (id) ->
# We need to exclude label IDs that will be applied
# By not doing this will cause issues from selection to not add labels at all
result.push(id) if labelsToApply.indexOf(id) is -1
result
......@@ -39,7 +39,7 @@ class @LabelsSelect
</a>
<% }); %>'
)
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
labelNoneHTMLTemplate = '<span class="no-value">None</span>'
if newLabelField.length
......@@ -145,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data)
labelCount = data.labels.length
else
template = labelNoneHTMLTemplate()
template = labelNoneHTMLTemplate
$value
.removeAttr('style')
.html(template)
......
class @LayoutNav
$ ->
hideEndFade = ($scrollingTabs) ->
$scrollingTabs.each ->
$this = $(@)
$this
.find('.fade-right')
.toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
$ ->
$('.fade-left').addClass('end-scroll')
hideEndFade($('.scrolling-tabs'))
$(window)
.off 'resize.nav'
.on 'resize.nav', ->
hideEndFade($('.scrolling-tabs'))
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this)
$el = $(event.target)
currentPosition = $this.scrollLeft()
size = bp.getBreakpointSize()
controlBtnWidth = $('.controls').width()
maxPosition = $this.get(0).scrollWidth - $this.parent().width()
maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
$el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
$this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
......@@ -23,6 +23,24 @@
return if @isInGroupsPage() then $('body').data 'group' else null
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
$tooltipEl
.tooltip 'destroy'
.attr 'title', newTitle
.tooltip 'fixTitle'
gl.utils.preventDisabledButtons = ->
$('.btn').click (e) ->
if $(this).hasClass 'disabled'
e.preventDefault()
e.stopImmediatePropagation()
return false
jQuery.timefor = (time, suffix, expiredLabel) ->
return '' unless time
......@@ -44,5 +62,4 @@
return timefor
) window
......@@ -42,9 +42,3 @@ work = ->
$(document).on('page:fetch', start)
$(document).on('page:change', stop)
$ ->
# Make logo clickable as part of a workaround for Safari visited
# link behaviour (See !2690).
$('#logo').on 'click', ->
Turbolinks.visit('/')
class @MergedButtons
constructor: ->
@$removeBranchWidget = $('.remove_source_branch_widget')
@$removeBranchProgress = $('.remove_source_branch_in_progress')
@$removeBranchFailed = $('.remove_source_branch_widget.failed')
@cleanEventListeners()
@initEventListeners()
cleanEventListeners: ->
$(document).off 'click', '.remove_source_branch'
$(document).off 'ajax:success', '.remove_source_branch'
$(document).off 'ajax:error', '.remove_source_branch'
initEventListeners: ->
$(document).on 'click', '.remove_source_branch', @removeSourceBranch
$(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
$(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
removeSourceBranch: =>
@$removeBranchWidget.hide()
@$removeBranchProgress.show()
removeBranchSuccess: ->
location.reload()
removeBranchError: ->
@$removeBranchWidget.hide()
@$removeBranchProgress.hide()
@$removeBranchFailed.show()
......@@ -24,14 +24,10 @@ class @MilestoneSelect
if issueUpdateURL
milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>">
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
<%= _.escape(title) %>
</span>
</a>'
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>'
)
milestoneLinkNoneTemplate = '<div class="light">None</div>'
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
collapsedSidebarLabelTemplate = _.template(
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
......@@ -116,7 +112,7 @@ class @MilestoneSelect
.val()
data = {}
data[abilityName] = {}
data[abilityName].milestone_id = selected
data[abilityName].milestone_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
......
# This is a manifest file that'll be compiled into including all the files listed below.
# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
# be included in the compiled file accessible from http://example.com/assets/application.js
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require raphael
#= require g.raphael
#= require g.bar
#= require_tree .
$ ->
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
commit_url: $(".network-graph").attr('data-commit-url'),
ref: $(".network-graph").attr('data-ref'),
commit_id: $(".network-graph").attr('data-commit-id')
})
new ShortcutsNetwork(network_graph.branch_graph)
......@@ -115,12 +115,14 @@ class @Notes
, @pollingInterval
refresh: =>
return if @refreshing is true
@refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
return if @refreshing
@refreshing = true
$.ajax
url: @notes_url
data: "last_fetched_at=" + @last_fetched_at
......
......@@ -43,6 +43,55 @@ class @Sidebar
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
$(document)
.off 'click', '.js-issuable-todo'
.on 'click', '.js-issuable-todo', @toggleTodo
toggleTodo: (e) =>
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
$.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}"
type: ajaxType
dataType: 'json'
data:
issuable_id: $this.data('issuable')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
).done (data) =>
@todoUpdateDone(data, $this, $btnText, $todoLoading)
beforeTodoSend: ($btn, $todoLoading) ->
$btn.disable()
$todoLoading.removeClass 'hidden'
todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
$todoPendingCount = $('.todos-pending-count')
$todoPendingCount.text data.count
$btn.enable()
$todoLoading.addClass 'hidden'
if data.count is 0
$todoPendingCount.addClass 'hidden'
else
$todoPendingCount.removeClass 'hidden'
if data.todo?
$btn
.attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
......@@ -117,5 +166,3 @@ class @Sidebar
getBlock: (name) ->
@sidebar.find(".block.#{name}")
class @Shortcuts
constructor: ->
constructor: (skipResetBindings) ->
@enabledHelp = []
Mousetrap.reset()
Mousetrap.reset() if not skipResetBindings
Mousetrap.bind('?', @onToggleHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
......
#= require shortcuts
class @ShortcutsBlob extends Shortcuts
constructor: (skipResetBindings) ->
super skipResetBindings
Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
@copyToClipboard: ->
clipboardButton = $('.btn-clipboard')
clipboardButton.click() if clipboardButton
......@@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
if $.cookie('pin_nav') is 'true'
$('.navbar-fixed-top').toggleClass('header-pinned-nav')
$('.page-with-sidebar').toggleClass('page-sidebar-pinned')
setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll();
niceScrollBars = $('.nav-sidebar').niceScroll();
niceScrollBars.updateScrollBar();
), 300
$(document)
.off 'click', 'body'
.on 'click', 'body', (e) ->
unless $.cookie('pin_nav') is 'true'
$target = $(e.target)
$nav = $target.closest('.sidebar-wrapper')
pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
$toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
if $nav.length is 0 and pageExpanded and $toggle.length is 0
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.toggleClass('header-collapsed header-expanded')
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
e.preventDefault()
......
......@@ -9,9 +9,11 @@ class @Star
$this.parent().find('.star-count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
gl.utils.updateTooltipTitle $this, 'Star project'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
else
$starSpan.addClass('starred').text 'Unstar'
gl.utils.updateTooltipTitle $this, 'Unstar project'
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
return
......
......@@ -31,7 +31,7 @@ class @UsersSelect
assignTo = (selected) ->
data = {}
data[abilityName] = {}
data[abilityName].assignee_id = selected
data[abilityName].assignee_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
......@@ -72,7 +72,7 @@ class @UsersSelect
assigneeTemplate = _.template(
'<% if (username) { %>
<a class="author_link " href="/u/<%= username %>">
<a class="author_link bold" href="/u/<%= username %>">
<% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %>
......@@ -82,7 +82,7 @@ class @UsersSelect
</span>
</a>
<% } else { %>
<span class="assign-yourself">
<span class="no-value assign-yourself">
No assignee -
<a href="#" class="js-assign-yourself">
assign yourself
......
......@@ -8,8 +8,8 @@
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.collapse-nav a {
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
background: $color;
......
......@@ -3,7 +3,7 @@
*
*/
header {
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
&.navbar-empty {
height: $header-height;
......@@ -79,14 +79,9 @@ header {
&.header-collapsed {
padding: 0 16px;
.side-nav-toggle {
display: block;
}
}
.side-nav-toggle {
display: none;
position: absolute;
left: -10px;
margin: 6px 0;
......@@ -108,9 +103,7 @@ header {
.header-content {
position: relative;
height: $header-height;
padding-right: 40px;
padding-left: 30px;
transition-duration: .3s;
@media (min-width: $screen-sm-min) {
padding-right: 0;
......@@ -198,18 +191,6 @@ header {
}
}
.header-collapsed {
margin-left: 0;
.header-content {
@media (min-width: $screen-sm-max) {
padding-left: 30px;
transition-duration: .3s;
}
}
}
.tanuki-shape {
transition: all 0.8s;
......
......@@ -159,7 +159,7 @@ ul.content-list {
background-color: $gray-light;
border: dotted 1px $gray-dark;
margin: 1px 0;
min-height: 30px;
min-height: 52px;
}
}
}
......
......@@ -74,6 +74,7 @@
.container-fluid {
background-color: $background-color;
margin-bottom: 0;
}
li {
......@@ -241,6 +242,12 @@
}
}
}
&.adjust {
.nav-text, .nav-controls {
width: auto;
}
}
}
.layout-nav {
......@@ -250,7 +257,7 @@
z-index: 11;
background: $background-color;
border-bottom: 1px solid $border-color;
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
text-align: center;
.container-fluid {
......@@ -280,11 +287,10 @@
}
.dropdown {
margin-left: 7px;
@media (max-width: $screen-xs-min) {
margin-left: 0;
}
position: absolute;
top: 7px;
right: 15px;
z-index: 2;
li.active {
font-weight: bold;
......@@ -347,6 +353,12 @@
.badge {
color: $gl-icon-color;
}
&:hover {
a, i {
color: $black;
}
}
}
}
......
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
left: 0;
height: 100%;
transition-duration: .3s;
overflow: hidden;
transition: width $sidebar-transition-duration;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $background-color;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
}
.content-wrapper {
width: 100%;
transition: padding $sidebar-transition-duration;
.container-fluid {
background: #fff;
......@@ -34,22 +39,19 @@
}
}
.sidebar-wrapper {
.sidebar-user {
padding: 15px 22px;
position: fixed;
.sidebar-user {
padding: 15px;
position: absolute;
left: 0;
bottom: 0;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px;
line-height: 34px;
}
line-height: 36px;
transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
@media (min-width: $sidebar-breakpoint) {
bottom: 50px;
}
}
......@@ -65,39 +67,46 @@
.nav-sidebar {
margin-top: 22 + $header-height;
margin-bottom: 116px;
transition-duration: .3s;
list-style: none;
overflow: hidden;
position: absolute;
top: 50px;
bottom: 65px;
width: $sidebar_width;
overflow-y: auto;
overflow-x: hidden;
@media (min-width: $sidebar-breakpoint) {
bottom: 115px;
}
&.navbar-collapse {
padding: 0 !important;
}
li {
width: $sidebar_width;
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a {
width: $sidebar_width;
padding: 7px 15px 7px 23px;
padding: 7px 15px 7px 12px;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
outline: none;
white-space: nowrap;
&:hover {
text-decoration: none;
}
&:active, &:focus {
&:hover,
&:active,
&:focus {
text-decoration: none;
}
......@@ -109,10 +118,6 @@
svg {
margin-right: 13px;
}
&.back-link i {
transition-duration: .3s;
}
}
}
......@@ -123,101 +128,86 @@
}
}
.sidebar-subnav {
margin-left: 0;
padding-left: 0;
li {
list-style: none;
}
}
.collapse-nav a {
.toggle-nav-collapse {
width: $sidebar_width;
position: fixed;
position: absolute;
top: 0;
left: 0;
min-height: 50px;
padding: 5px 0;
font-size: 18px;
background: transparent;
height: 50px;
text-align: center;
line-height: 40px;
line-height: 30px;
}
.nav-header-btn {
padding: 10px 5px;
color: inherit;
transition-duration: .3s;
outline: none;
&:hover {
&:hover,
&:focus {
color: $white-light;
text-decoration: none;
}
}
.sidebar-wrapper {
&.hidden-nav {
width: 0;
}
}
.page-sidebar-collapsed {
padding-left: 0;
.sidebar-wrapper {
width: 0;
.nav-sidebar {
width: 0;
li {
width: auto;
a {
span {
.pin-nav-btn {
display: none;
}
}
}
position: absolute;
left: 0;
bottom: 0;
height: 50px;
width: $sidebar_width;
line-height: 30px;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.collapse-nav a {
width: 0;
.fa {
transition: transform .15s;
}
i {
display: none;
&.is-active {
.fa {
transform: rotate(90deg);
}
}
}
.sidebar-user {
width: 0;
.page-sidebar-collapsed {
padding-left: 0;
padding-right: 0;
.username {
display: none;
}
}
.sidebar-wrapper {
width: 0;
}
}
.page-sidebar-expanded {
@media (max-width: $screen-sm-max) {
padding-left: 0;
}
.sidebar-wrapper {
width: $sidebar_width;
}
}
.nav-sidebar {
width: $sidebar_width;
.page-sidebar-pinned {
.content-wrapper,
.layout-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
}
}
}
.nav-sidebar li a {
width: $sidebar_width;
header.header-pinned-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
&.back-link {
i {
opacity: 0;
}
.side-nav-toggle {
display: none;
}
.header-content {
padding-left: 0;
}
}
}
......
......@@ -6,6 +6,8 @@ $sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1440px;
/*
* UI elements
......
......@@ -7,84 +7,111 @@
margin-right: 9px;
}
.lists-separator {
margin: 10px 0;
border-color: #ddd;
}
.commits-row {
ul {
margin: 0;
li.commit {
padding: 8px 0;
}
}
.commit-header {
padding: 5px 10px;
background-color: $background-color;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
font-size: 14px;
.commits-row-date {
font-size: 15px;
line-height: 20px;
margin-bottom: 5px;
&:first-child {
border-top-width: 0;
}
}
li.commit {
list-style: none;
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
margin-bottom: 2px;
.btn-clipboard {
margin-top: -1px;
}
.commit-row-title {
line-height: 1;
margin-bottom: 7px;
.notes_count {
float: right;
margin-right: 10px;
}
.commit_short_id {
min-width: 65px;
color: $gl-dark-link-color;
font-family: $monospace_font;
}
.str-truncated {
max-width: 70%;
}
.commit-row-message {
color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
}
}
.text-expander {
background: #eee;
color: #555;
display: inline-block;
background: $gray-light;
color: $gl-placeholder-color;
padding: 0 5px;
cursor: pointer;
margin-left: 4px;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
&:hover {
background-color: #ddd;
background-color: darken($gray-light, 10%);
text-decoration: none;
}
}
}
.commit-actions {
@media (min-width: $screen-sm-min) {
float: right;
margin-left: $gl-padding;
margin-top: 2px;
font-size: 0;
}
.btn-transparent {
padding-left: 0;
padding-right: 0;
}
.btn {
&:not(:first-child) {
margin-left: $gl-padding;
}
}
}
.commit-short-id {
font-family: $monospace_font;
font-weight: 600;
}
.commit {
padding: 10px 0;
@media (min-width: $screen-sm-min) {
padding-left: 46px;
}
&:not(:last-child) {
border-bottom: 1px solid #eee;
}
a,
button {
color: $gl-dark-link-color;
vertical-align: baseline;
}
.avatar {
margin-left: -46px;
}
.item-title {
display: inline-block;
@media (min-width: $screen-sm-min) {
max-width: 70%;
}
}
.commit-row-description {
font-size: 14px;
border-left: 1px solid #eee;
padding: 10px 15px;
margin: 5px 0 10px 5px;
margin: 10px 0;
background: #f9f9f9;
display: none;
......@@ -102,7 +129,7 @@ li.commit {
.commit-row-info {
color: $gl-gray;
line-height: 24px;
line-height: 1;
a {
color: $gl-gray;
......@@ -111,10 +138,6 @@ li.commit {
.avatar {
margin-right: 8px;
}
.committed_ago {
display: inline-block;
}
}
&.inline-commit {
......
.environments {
.commit-title {
margin: 0;
}
}
......@@ -39,3 +39,20 @@
}
}
}
.groups-cover-block {
.container-fluid {
position: relative;
}
.access-request-button {
@include btn-gray;
position: absolute;
right: 16px;
bottom: 32px;
padding: 3px 10px;
text-transform: none;
background-color: $background-color;
}
}
......@@ -34,6 +34,10 @@
color: inherit;
}
.issuable-header-text {
margin-top: 7px;
}
.block {
@include clearfix;
padding: $gl-padding 0;
......@@ -60,10 +64,6 @@
margin-top: 0;
}
.issuable-count {
margin-top: 7px;
}
.gutter-toggle {
margin-left: 20px;
padding-left: 10px;
......@@ -145,7 +145,6 @@
.assign-yourself {
margin-top: 10px;
font-weight: normal;
display: block;
}
}
......@@ -158,6 +157,10 @@
font-weight: normal;
}
.no-value {
color: $gl-placeholder-color;
}
.sidebar-collapsed-icon {
display: none;
}
......@@ -250,7 +253,7 @@
}
}
.issuable-pager {
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
......@@ -263,7 +266,7 @@
}
}
a:not(.issuable-pager) {
a {
&:hover {
color: $md-link-color;
text-decoration: none;
......@@ -322,7 +325,7 @@
margin-left: 5px;
a {
color: #8c8c8c;
color: $gl-placeholder-color;
}
}
......
......@@ -115,6 +115,13 @@
}
}
.draggable-handler {
display: inline-block;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
}
.prioritized-labels {
margin-bottom: 30px;
......@@ -122,6 +129,13 @@
display: none;
color: $gray-light;
}
li:hover {
.draggable-handler {
display: inline-block;
opacity: 1;
}
}
}
.other-labels {
......
......@@ -313,3 +313,13 @@
}
}
}
.merged-buttons {
.btn {
float: left;
&:not(:last-child) {
margin-right: 10px;
}
}
}
......@@ -139,6 +139,12 @@ ul.notes {
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
}
.note-emoji-button {
......@@ -259,6 +265,10 @@ ul.notes {
right: 0;
top: 0;
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) {
position: relative;
}
......
......@@ -5,10 +5,12 @@
font-weight: normal;
}
}
.no-ssh-key-message, .project-limit-message {
background-color: #f28d35;
margin-bottom: 0;
}
.new_project,
.edit-project {
fieldset.features {
......@@ -18,13 +20,6 @@
}
}
.project-name-holder {
.help-inline {
vertical-align: top;
padding: 7px;
}
}
.project-home-panel {
background: $white-light;
text-align: left;
......@@ -229,13 +224,20 @@
right: 16px;
bottom: 0;
.btn {
padding: 3px 10px;
background-color: $background-color;
@media (max-width: $screen-lg-min) {
top: 0;
}
@media (max-width: 1304px) {
top: 0;
.access-request-button {
position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
position: relative;
bottom: 0;
margin-right: 10px;
}
}
}
......@@ -286,10 +288,6 @@
color: #555;
}
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container {
min-width: 200px;
}
......@@ -373,6 +371,7 @@ a.deploy-project-label {
.project-import .btn {
float: left;
margin-bottom: 10px;
margin-right: 10px;
}
......
......@@ -101,7 +101,7 @@
margin: 0;
.commit {
padding: 0;
padding: 0 0 0 55px;
.commit-row-title {
.commit-row-message {
......@@ -129,4 +129,6 @@
.tree-controls {
float: right;
margin-top: 11px;
position: relative;
z-index: 2;
}
......@@ -35,6 +35,7 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id])
projects = current_user.authorized_projects
projects = projects.search(params[:search]) if params[:search].present?
projects = projects.select do |project|
current_user.can?(:admin_issue, project)
end
......
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
redirect_to polymorphic_path(membershipable),
notice: 'Your request for access has been queued for review.'
end
def approve_access_request
@member = membershipable.members.request.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
redirect_to polymorphic_url([membershipable, :members])
end
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
else
render_403
end
end
end
protected
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
end
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave]
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
def leave
@group_member = @group.group_members.find_by(user_id: current_user)
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
else
return render_403
end
end
end
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end
......@@ -42,7 +42,7 @@ class JwtController < ApplicationController
end
def authenticate_user(login, password)
user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
user = Gitlab::Auth.find_with_user_password(login, password)
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user
end
......
class Projects::ArtifactsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :validate_artifacts!
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
unless artifacts_file.exists?
return render_404
end
send_file artifacts_file.path, disposition: 'attachment'
end
def browse
return render_404 unless build.artifacts?
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
......@@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
end
def keep
build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build)
end
private
def validate_artifacts!
render_404 unless build.artifacts?
end
def build
@build ||= project.builds.find_by!(id: params[:build_id])
end
......
......@@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController
return render_404
end
build = Ci::Build.retry(@build)
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
......
......@@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController
def retry_builds
ci_builds.latest.failed.each do |build|
if build.retryable?
Ci::Build.retry(build)
Ci::Build.retry(build, current_user)
end
end
......
class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_update_environment!, only: [:destroy]
before_action :environment, only: [:show, :destroy]
def index
@environments = project.environments
end
def show
@deployments = environment.deployments.order(id: :desc).page(params[:page])
end
def new
@environment = project.environments.new
end
def create
@environment = project.environments.create(create_params)
if @environment.persisted?
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
else
render 'new'
end
end
def destroy
if @environment.destroy
flash[:notice] = 'Environment was successfully removed.'
else
flash[:alert] = 'Failed to remove environment.'
end
redirect_to namespace_project_environments_path(project.namespace, project)
end
private
def create_params
params.require(:environment).permit(:name)
end
def environment
@environment ||= project.environments.find(params[:id])
end
end
......@@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController
return if project && project.public? && upload_pack?
authenticate_or_request_with_http_basic do |login, password|
auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip)
auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
if auth_result.type == :ci && upload_pack?
@ci = true
......
......@@ -204,10 +204,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
if params[:merge_when_build_succeeds].present?
if @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
elsif @merge_request.pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
else
@status = :failed
end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
......
......@@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def retry
pipeline.retry_failed
pipeline.retry_failed(current_user)
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
......
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
# Authorize
before_action :authorize_admin_project_member!, except: [:leave, :index]
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
@project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
......@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC')
@group = @project.group
if @group
@group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
@group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
def leave
@project_member = @project.project_members.find_by(user_id: current_user)
if can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
respond_to do |format|
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { head :ok }
end
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
else
render_403
end
end
end
def apply_import
source_project = Project.find(params[:source_project_id])
......@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end
class Projects::TodosController < Projects::ApplicationController
def create
todos = TodoService.new.mark_todo(issuable, current_user)
render json: {
todo: todos,
count: current_user.todos.pending.count,
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
count: current_user.todos.pending.count,
}
end
private
def issuable
@issuable ||= begin
case params[:issuable_type]
when "issue"
@project.issues.find(params[:issuable_id])
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
end
end
end
......@@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
if file.on_disk?
send_file file.on_disk_path, disposition: 'inline'
else
......
......@@ -40,7 +40,7 @@ class SessionsController < Devise::SessionsController
# Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change.
def check_initial_setup
return unless User.count == 1
return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
user = User.admins.last
......
......@@ -12,7 +12,7 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
project.issues.find(target_id).notes.inc_author
project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
......
......@@ -51,7 +51,7 @@ class SnippetsFinder
snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id) || current_user.admin?
if project.team.member?(current_user) || current_user.admin?
snippets
else
snippets.public_and_internal
......
......@@ -36,7 +36,7 @@ class TodosFinder
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
end
def action_id
......
......@@ -116,7 +116,7 @@ module BlobHelper
end
def blob_text_viewable?(blob)
blob && blob.text? && !blob.lfs_pointer?
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
end
def blob_size(blob)
......
......@@ -17,7 +17,15 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
class: 'btn btn-clipboard',
class: "btn",
data: data,
type: :button
end
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data,
type: :button
end
......
......@@ -38,10 +38,10 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
def render_commit_status(commit, tooltip_placement: 'auto left')
def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement)
render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
......@@ -57,10 +57,10 @@ module CiStatusHelper
private
def render_status_with_link(type, status, path, tooltip_placement)
def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
link_to ci_icon_for_status(status),
path,
class: "ci-status-link ci-status-icon-#{status.dasherize}",
class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
......
......@@ -16,6 +16,16 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
def commit_author_avatar(commit, options = {})
options = options.merge(source: :author)
user = commit.send(options[:source])
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_email = user.try(:email) || source_email
image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
end
def image_diff_class(diff)
if diff.deleted_file
"deleted"
......@@ -102,24 +112,24 @@ module CommitsHelper
if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to(
"Browse File »",
"Browse File",
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
class: "btn btn-default"
)
elsif @path.present?
return link_to(
"Browse Directory »",
"Browse Directory",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
class: "btn btn-default"
)
end
end
link_to(
"Browse Files",
namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right"
class: "btn btn-default"
)
end
......@@ -129,7 +139,7 @@ module CommitsHelper
tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
if can_collaborate_with_project?
btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil?
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
......@@ -141,7 +151,7 @@ module CommitsHelper
namespace_key: current_user.namespace.id,
continue: continue_params)
btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
......@@ -153,7 +163,7 @@ module CommitsHelper
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project?
btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil?
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
......@@ -187,12 +197,10 @@ module CommitsHelper
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name
person_email = user.try(:email) || source_email
text =
if options[:avatar]
avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
%Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
else
person_name
end
......
......@@ -135,6 +135,11 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
def diff_compare_whitespace_link(project, from, to, options)
url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace)
toggle_whitespace_link(url, options)
end
private
def hide_whitespace?
......
......@@ -13,10 +13,23 @@
# merge_request_path(merge_request)
#
module GitlabRoutingHelper
# Project
def project_path(project, *args)
namespace_project_path(project.namespace, project, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end
......@@ -29,6 +42,10 @@ module GitlabRoutingHelper
namespace_project_pipelines_path(project.namespace, project, *args)
end
def project_environments_path(project, *args)
namespace_project_environments_path(project.namespace, project, *args)
end
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
......@@ -41,10 +58,6 @@ module GitlabRoutingHelper
activity_namespace_project_path(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args)
end
......@@ -65,14 +78,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end
......@@ -92,4 +97,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
end
def project_member_path(project_member, *args)
namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def request_access_project_members_path(project, *args)
request_access_namespace_project_project_members_path(project.namespace, project)
end
def leave_project_members_path(project, *args)
leave_namespace_project_project_members_path(project.namespace, project)
end
def approve_access_request_project_member_path(project_member, *args)
approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def resend_invite_project_member_path(project_member, *args)
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
# Groups
## Members
def group_members_url(group, *args)
group_group_members_url(group, *args)
end
def group_member_path(group_member, *args)
group_group_member_path(group_member.source, group_member)
end
def request_access_group_members_path(group, *args)
request_access_group_group_members_path(group)
end
def leave_group_members_path(group, *args)
leave_group_group_members_path(group)
end
def approve_access_request_group_member_path(group_member, *args)
approve_access_request_group_group_member_path(group_member.source, group_member)
end
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
end
module GroupsHelper
def remove_user_from_group_message(group, member)
if member.user
"Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
else
"Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
end
end
def leave_group_message(group)
"Are you sure you want to leave \"#{group}\" group?"
end
def should_user_see_group_roles?(user, group)
if user
user.is_admin? || group.members.exists?(user_id: user.id)
else
false
end
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
......
......@@ -67,6 +67,12 @@ module IssuablesHelper
end
end
def has_todo(issuable)
unless current_user.nil?
current_user.todos.find_by(target_id: issuable.id, state: :pending)
end
end
private
def sidebar_gutter_collapsed?
......
module MembersHelper
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member
def action_member_permission(action, member)
"#{action}_#{member.type.underscore}".to_sym
end
def can_see_member_roles?(source:, user: nil)
return false unless user
user.is_admin? || source.members.exists?(user_id: user.id)
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
text = 'Are you sure you want to '
action =
if member.request?
if member.user == user
'withdraw your access request for'
else
"deny #{member.user.name}'s request to join"
end
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else
"remove #{member.user.name} from"
end
text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
end
def remove_member_title(member)
text = " from #{member.real_source_type.humanize(capitalize: false)}"
text.prepend(member.request? ? 'Deny access request' : 'Remove user')
end
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
end
......@@ -12,10 +12,10 @@ module NavHelper
end
def page_sidebar_class
if nav_menu_collapsed?
"page-sidebar-collapsed"
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
else
"page-sidebar-expanded"
"page-sidebar-collapsed"
end
end
......@@ -36,7 +36,15 @@ module NavHelper
end
def nav_header_class
class_name = " with-horizontal-nav" if defined?(nav) && nav
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-expanded header-pinned-nav"
else
class_name << " header-collapsed"
end
class_name
end
......@@ -47,4 +55,8 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end
module ProjectsHelper
def remove_from_project_team_message(project, member)
if member.user
"You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
else
"You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
end
end
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
......@@ -49,7 +41,7 @@ module ProjectsHelper
author_html = author_html.html_safe
if opts[:name]
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
......@@ -115,14 +107,6 @@ 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
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
......@@ -156,6 +140,10 @@ module ProjectsHelper
nav_tabs << :container_registry
end
if can?(current_user, :read_environment, project)
nav_tabs << :environments
end
if can?(current_user, :admin_project, project)
nav_tabs << :settings
end
......@@ -286,10 +274,6 @@ module ProjectsHelper
end
end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
def new_readme_path
ref = @repository.root_ref if @repository
ref ||= 'master'
......
......@@ -20,7 +20,6 @@ module TimeHelper
end
end
def date_from_to(from, to)
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
......
......@@ -12,6 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for'
end
end
......
module Emails
module Groups
def group_access_granted_email(group_member_id)
@group_member = GroupMember.find(group_member_id)
@group = @group_member.group
@target_url = group_url(@group)
@current_user = @group_member.user
mail(to: @group_member.user.notification_email,
subject: subject("Access to group was granted"))
end
def group_member_invited_email(group_member_id, token)
@group_member = GroupMember.find group_member_id
@group = @group_member.group
@token = token
@target_url = group_url(@group)
@current_user = @group_member.user
mail(to: @group_member.invite_email,
subject: "Invitation to join group #{@group.name}")
end
def group_invite_accepted_email(group_member_id)
@group_member = GroupMember.find group_member_id
return if @group_member.created_by.nil?
@group = @group_member.group
@target_url = group_url(@group)
@current_user = @group_member.created_by
mail(to: @group_member.created_by.notification_email,
subject: subject("Invitation accepted"))
end
def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
return if created_by_id.nil?
@group = Group.find(group_id)
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = group_url(@group)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
end
end
module Emails
module Members
extend ActiveSupport::Concern
include MembersHelper
included do
helper_method :member_source, :member
end
def member_access_requested_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
mail(to: admins,
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_access_granted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
mail(to: member.user.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
end
def member_access_denied_email(member_source_type, source_id, user_id)
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
requester = User.find(user_id)
mail(to: requester.notification_email,
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
end
def member_invited_email(member_source_type, member_id, token)
@member_source_type = member_source_type
@member_id = member_id
@token = token
mail(to: member.invite_email,
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
end
def member_invite_accepted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
return unless member.created_by
mail(to: member.created_by.notification_email,
subject: subject('Invitation accepted'))
end
def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
return unless created_by_id
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
@invite_email = invite_email
inviter = User.find(created_by_id)
mail(to: inviter.notification_email,
subject: subject('Invitation declined'))
end
def member
@member ||= Member.find(@member_id)
end
def member_source
@member_source ||= member.source
end
private
def member_source_class
@member_source_type.classify.constantize
end
end
end
module Emails
module Projects
def project_access_granted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
mail(to: @project_member.user.notification_email,
subject: subject("Access to project was granted"))
end
def project_member_invited_email(project_member_id, token)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@token = token
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
mail(to: @project_member.invite_email,
subject: "Invitation to join project #{@project.name_with_namespace}")
end
def project_invite_accepted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
return if @project_member.created_by.nil?
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
mail(to: @project_member.created_by.notification_email,
subject: subject("Invitation accepted"))
end
def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
return if created_by_id.nil?
@project = Project.find(project_id)
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = namespace_project_url(@project.namespace, @project)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id
@project = Project.find project_id
......
......@@ -6,13 +6,15 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
include Emails::Groups
include Emails::Builds
include Emails::Members
add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper
add_template_helper MembersHelper
add_template_helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
......
......@@ -9,7 +9,6 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject)
when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject)
......@@ -19,6 +18,7 @@ class Ability
when GroupMember then group_member_abilities(user, subject)
when ProjectMember then project_member_abilities(user, subject)
when User then user_abilities
when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
else []
end.concat(global_abilities(user))
end
......@@ -187,6 +187,8 @@ class Ability
project_report_rules
elsif team.guest?(user)
project_guest_rules
else
[]
end
end
......@@ -228,6 +230,8 @@ class Ability
:read_build,
:read_container_image,
:read_pipeline,
:read_environment,
:read_deployment
]
end
......@@ -246,6 +250,8 @@ class Ability
:push_code,
:create_container_image,
:update_container_image,
:create_environment,
:create_deployment
]
end
......@@ -263,6 +269,8 @@ class Ability
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
:update_environment,
:update_deployment,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
......@@ -273,7 +281,9 @@ class Ability
:admin_commit_status,
:admin_build,
:admin_container_image,
:admin_pipeline
:admin_pipeline,
:admin_environment,
:admin_deployment
]
end
......@@ -317,6 +327,8 @@ class Ability
unless project.builds_enabled
rules += named_abilities('build')
rules += named_abilities('pipeline')
rules += named_abilities('environment')
rules += named_abilities('deployment')
end
unless project.container_registry_enabled
......@@ -511,10 +523,6 @@ class Ability
end
end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private
def restricted_public_level?
......@@ -533,7 +541,7 @@ class Ability
def filter_confidential_issues_abilities(user, issue, rules)
return rules if user.admin? || !issue.confidential?
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
rules.delete(:admin_issue)
rules.delete(:read_issue)
rules.delete(:update_issue)
......
......@@ -24,7 +24,7 @@ class Blob < SimpleDelegator
end
def only_display_raw?
size && size > 5.megabytes
size && truncated?
end
def svg?
......
......@@ -11,6 +11,8 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
......@@ -38,7 +40,7 @@ module Ci
new_build.save
end
def retry(build)
def retry(build, user = nil)
new_build = Ci::Build.new(status: 'pending')
new_build.ref = build.ref
new_build.tag = build.tag
......@@ -52,6 +54,7 @@ module Ci
new_build.stage = build.stage
new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
new_build.user = user
new_build.save
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
......@@ -73,6 +76,17 @@ module Ci
build.update_coverage
build.execute_hooks
end
after_transition any => [:success] do |build|
if build.environment.present?
service = CreateDeploymentService.new(build.project, build.user,
environment: build.environment,
sha: build.sha,
ref: build.ref,
tag: build.tag)
service.execute(build)
end
end
end
def retryable?
......@@ -83,10 +97,6 @@ module Ci
!self.pipeline.statuses.latest.include?(self)
end
def retry
Ci::Build.retry(self)
end
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
......@@ -317,7 +327,7 @@ module Ci
end
def artifacts?
artifacts_file.exists?
!artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
......@@ -328,11 +338,15 @@ module Ci
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
end
def erase_artifacts!
remove_artifacts_file!
remove_artifacts_metadata!
end
def erase(opts = {})
return false unless erasable?
remove_artifacts_file!
remove_artifacts_metadata!
erase_artifacts!
erase_trace!
update_erased!(opts[:erased_by])
end
......@@ -345,6 +359,25 @@ module Ci
!self.erased_at.nil?
end
def artifacts_expired?
artifacts_expire_at && artifacts_expire_at < Time.now
end
def artifacts_expire_in
artifacts_expire_at - Time.now if artifacts_expire_at
end
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
Time.now + ChronicDuration.parse(value)
end
end
def keep_artifacts!
self.update(artifacts_expire_at: nil)
end
private
def erase_trace!
......@@ -352,7 +385,7 @@ module Ci
end
def update_erased!(user = nil)
self.update(erased_by: user, erased_at: Time.now)
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
def yaml_variables
......
......@@ -76,8 +76,10 @@ module Ci
builds.running_or_pending.each(&:cancel)
end
def retry_failed
builds.latest.failed.select(&:retryable?).each(&:retry)
def retry_failed(user)
builds.latest.failed.select(&:retryable?).each do |build|
Ci::Build.retry(build, user)
end
end
def latest?
......@@ -161,6 +163,10 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
private
def update_state
......
# == AccessRequestable concern
#
# Contains functionality related to objects that can receive request for access.
#
# Used by Project, and Group.
#
module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
members.create(
access_level: Gitlab::Access::DEVELOPER,
user: user,
requested_at: Time.now.utc)
end
end
......@@ -5,7 +5,7 @@ module Awardable
has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable
participant :award_emoji
participant :award_emoji_with_associations
end
end
......@@ -34,8 +34,12 @@ module Awardable
end
end
def award_emoji_with_associations
award_emoji.includes(:user)
end
def grouped_awards(with_thumbs: true)
awards = award_emoji.group_by(&:name)
awards = award_emoji_with_associations.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= []
......
class Deployment < ActiveRecord::Base
include InternalId
belongs_to :project, required: true, validate: true
belongs_to :environment, required: true, validate: true
belongs_to :user
belongs_to :deployable, polymorphic: true
validates :sha, presence: true
validates :ref, presence: true
delegate :name, to: :environment, prefix: true
def commit
project.commit(sha)
end
def commit_title
commit.try(:title)
end
def short_sha
Commit.truncate_sha(sha)
end
def last?
self == environment.last_deployment
end
end
class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
has_many :deployments
validates :name,
presence: true,
uniqueness: { scope: :project_id },
length: { within: 0..255 },
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
def last_deployment
deployments.last
end
end
......@@ -3,11 +3,12 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include AccessRequestable
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
......@@ -58,6 +59,10 @@ class Group < Namespace
"#{self.class.reference_prefix}#{name}"
end
def web_url
Gitlab::Routing.url_helpers.group_url(self)
end
def human_name
name
end
......
......@@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base
end
def self.visible_to_user(user)
return where(confidential: false) if user.blank?
return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return all if user.admin?
where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR issues.assignee_id = :user_id
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def self.reference_prefix
......
......@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base
allow_nil: true
}
scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") }
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
scope :non_pending, -> { non_request.non_invite }
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite?
after_create :post_create_hook, unless: :invite?
after_update :post_update_hook, unless: :invite?
after_destroy :post_destroy_hook, unless: :invite?
after_create :send_request, if: :request?
after_create :create_notification_setting, unless: :pending?
after_create :post_create_hook, unless: :pending?
after_update :post_update_hook, unless: :pending?
after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request?
delegate :name, :username, :email, to: :user, prefix: true
......@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base
end
end
def real_source_type
source_type
end
def invite?
self.invite_token.present?
end
def request?
requested_at.present?
end
def pending?
invite? || request?
end
def accept_request
return false unless request?
updated = self.update(requested_at: nil)
after_accept_request if updated
updated
end
def accept_invite!(new_user)
return false unless invite?
......@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base
# override in subclass
end
def send_request
# override in subclass
end
def post_create_hook
system_hook_service.execute_hooks_for(self, :create)
end
......@@ -177,6 +210,14 @@ class Member < ActiveRecord::Base
# override in subclass
end
def after_accept_request
post_create_hook
end
def post_decline_request
# override in subclass
end
def system_hook_service
SystemHooksService.new
end
......
......@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
def self.access_level_roles
Gitlab::Access.options_with_owner
end
......@@ -23,6 +20,11 @@ class GroupMember < Member
access_level
end
# Because source_type is `Namespace`...
def real_source_type
'Group'
end
private
def send_invite
......@@ -31,6 +33,12 @@ class GroupMember < Member
super
end
def send_request
notification_service.new_group_access_request(self)
super
end
def post_create_hook
notification_service.new_group_member(self)
......@@ -56,4 +64,10 @@ class GroupMember < Member
super
end
def post_decline_request
notification_service.decline_group_access_request(self)
super
end
end
......@@ -11,8 +11,6 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
scope :with_user, ->(user) { where(user_id: user.id) }
before_destroy :delete_member_todos
......@@ -84,7 +82,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options
end
def access_roles
def access_level_roles
Gitlab::Access.options
end
end
......@@ -113,6 +111,12 @@ class ProjectMember < Member
super
end
def send_request
notification_service.new_project_access_request(self)
super
end
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
......@@ -148,6 +152,12 @@ class ProjectMember < Member
super
end
def post_decline_request
notification_service.decline_project_access_request(self)
super
end
def event_service
EventCreateService.new
end
......
......@@ -88,22 +88,9 @@ class Note < ActiveRecord::Base
table = arel_table
pattern = "%#{query}%"
found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id').
where(table[:note].matches(pattern))
if as_user
found_notes.where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
OR (issues.confidential IS TRUE
AND (issues.author_id = :user_id
OR issues.assignee_id = :user_id
OR issues.project_id IN(:project_ids)))',
user_id: as_user.id,
project_ids: as_user.authorized_projects.select(:id))
else
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
end
Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
where(table[:note].matches(pattern)).
merge(Issue.visible_to_user(as_user))
end
end
......@@ -200,6 +187,10 @@ class Note < ActiveRecord::Base
award_emoji_supported? && contains_emoji_only?
end
def emoji_awardable?
!system?
end
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
......
......@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
include Referable
include Sortable
include AfterCommitQueue
......@@ -80,7 +81,7 @@ class Project < ActiveRecord::Base
has_one :jira_service, dependent: :destroy
has_one :redmine_service, dependent: :destroy
has_one :custom_issue_tracker_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
......@@ -103,7 +104,8 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members
alias_method :members, :project_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
......@@ -125,6 +127,8 @@ class Project < ActiveRecord::Base
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
......@@ -146,7 +150,6 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
......@@ -589,10 +592,6 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
def can_have_issues_tracker_id?
self.issues_enabled && !self.default_issues_tracker?
end
def build_missing_services
services_templates = Service.where(template: true)
......@@ -685,16 +684,6 @@ class Project < ActiveRecord::Base
end
end
def project_member_by_name_or_email(name = nil, email = nil)
user = users.find_by('name like ? or email like ?', name, email)
project_members.where(user: user) if user
end
# Get Team Member record by user id
def project_member_by_id(user_id)
project_members.find_by(user_id: user_id)
end
def name_with_namespace
@name_with_namespace ||= begin
if namespace
......@@ -704,6 +693,7 @@ class Project < ActiveRecord::Base
end
end
end
alias_method :human_name, :name_with_namespace
def path_with_namespace
if namespace
......
class BambooService < CiService
include HTTParty
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
......@@ -61,18 +59,7 @@ class BambooService < CiService
end
def build_info(sha)
url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
if username.blank? && password.blank?
@response = HTTParty.get(url, verify: false)
else
url << '&os_authType=basic'
auth = {
username: username,
password: password
}
@response = HTTParty.get(url, verify: false, basic_auth: auth)
end
@response = get_path("rest/api/latest/result?label=#{sha}")
end
def build_page(sha, ref)
......@@ -80,11 +67,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
URI.join(bamboo_url, "/browse/#{build_key}").to_s
URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key']
URI.join(bamboo_url, "/browse/#{result_key}").to_s
URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
......@@ -112,8 +99,27 @@ class BambooService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
# Bamboo requires a GET and does not take any data.
url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
self.class.get(url, verify: false)
get_path("updateAndBuild.action?buildKey=#{build_key}")
end
private
def build_url(path)
URI.join("#{bamboo_url}/", path).to_s
end
def get_path(path)
url = build_url(path)
if username.blank? && password.blank?
HTTParty.get(url, verify: false)
else
url << '&os_authType=basic'
HTTParty.get(url, verify: false,
basic_auth: {
username: username,
password: password
})
end
end
end
......@@ -38,9 +38,9 @@ class IssueTrackerService < Service
if enabled_in_gitlab_config
self.properties = {
title: issues_tracker['title'],
project_url: add_issues_tracker_id(issues_tracker['project_url']),
issues_url: add_issues_tracker_id(issues_tracker['issues_url']),
new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url'])
project_url: issues_tracker['project_url'],
issues_url: issues_tracker['issues_url'],
new_issue_url: issues_tracker['new_issue_url']
}
else
self.properties = {}
......@@ -83,16 +83,4 @@ class IssueTrackerService < Service
def issues_tracker
Gitlab.config.issues_tracker[to_param]
end
def add_issues_tracker_id(url)
if self.project
id = self.project.issues_tracker_id
if id
url = url.gsub(":issues_tracker_id", id)
end
end
url
end
end
class TeamcityService < CiService
include HTTParty
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
......@@ -64,15 +62,7 @@ class TeamcityService < CiService
end
def build_info(sha)
url = URI.join(
teamcity_url,
"/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
).to_s
auth = {
username: username,
password: password
}
@response = HTTParty.get(url, verify: false, basic_auth: auth)
@response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
end
def build_page(sha, ref)
......@@ -81,14 +71,11 @@ class TeamcityService < CiService
if @response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = @response['build']['id']
URI.join(
teamcity_url,
"/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
).to_s
build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
......@@ -123,8 +110,8 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
self.class.post(
URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
HTTParty.post(
build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
'</build>',
......@@ -132,4 +119,18 @@ class TeamcityService < CiService
basic_auth: auth
)
end
private
def build_url(path)
URI.join("#{teamcity_url}/", path).to_s
end
def get_path(path)
HTTParty.get(build_url(path), verify: false,
basic_auth: {
username: username,
password: password
})
end
end
......@@ -21,23 +21,13 @@ class ProjectTeam
end
end
def find(user_id)
user = project.users.find_by(id: user_id)
if group
user ||= group.users.find_by(id: user_id)
end
user
end
def find_member(user_id)
member = project.project_members.find_by(user_id: user_id)
member = project.members.non_request.find_by(user_id: user_id)
# If user is not in project members
# we should check for group membership
if group && !member
member = group.group_members.find_by(user_id: user_id)
member = group.members.non_request.find_by(user_id: user_id)
end
member
......@@ -61,13 +51,10 @@ class ProjectTeam
ProjectMember.truncate_team(project)
end
def users
members
end
def members
@members ||= fetch_members
end
alias_method :users, :members
def guests
@guests ||= fetch_members(:guests)
......@@ -131,8 +118,14 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
def member?(user_id)
!!find_member(user_id)
def member?(user, min_member_access = nil)
member = !!find_member(user.id)
if min_member_access
member && max_member_access(user.id) >= min_member_access
else
member
end
end
def human_max_access(user_id)
......@@ -144,7 +137,7 @@ class ProjectTeam
def max_member_access(user_id)
access = []
project.project_members.each do |member|
project.members.non_request.each do |member|
if member.user_id == user_id
access << member.access_field if member.access_field
break
......@@ -152,7 +145,7 @@ class ProjectTeam
end
if group
group.group_members.each do |member|
group.members.non_request.each do |member|
if member.user_id == user_id
access << member.access_field if member.access_field
break
......@@ -167,6 +160,7 @@ class ProjectTeam
access.compact.max
end
private
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
......@@ -183,17 +177,15 @@ class ProjectTeam
end.compact.max
end
private
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
project_members = project.members.non_request
group_members = group ? group.members.non_request : []
invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group?
project.project_group_links.each do |group_link|
invited_group = group_link.group
im = invited_group.group_members
im = invited_group.members.non_request
if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
......
......@@ -446,7 +446,7 @@ class Repository
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
Gitlab::Git::Blob.find(self, sha, path)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
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.
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