summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG14
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee6
-rw-r--r--app/assets/javascripts/issue.js.coffee25
-rw-r--r--app/assets/javascripts/issues.js.coffee26
-rw-r--r--app/assets/javascripts/labels_select.js.coffee76
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee51
-rw-r--r--app/assets/javascripts/notes.js.coffee5
-rw-r--r--app/assets/javascripts/project_new.js.coffee13
-rw-r--r--app/assets/javascripts/todos.js.coffee56
-rw-r--r--app/assets/javascripts/users_select.js.coffee35
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss24
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss6
-rw-r--r--app/assets/stylesheets/framework/filters.scss17
-rw-r--r--app/assets/stylesheets/framework/lists.scss11
-rw-r--r--app/assets/stylesheets/framework/variables.scss82
-rw-r--r--app/assets/stylesheets/pages/appearances.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss10
-rw-r--r--app/assets/stylesheets/pages/events.scss13
-rw-r--r--app/assets/stylesheets/pages/issuable.scss151
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/login.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss17
-rw-r--r--app/assets/stylesheets/pages/projects.scss15
-rw-r--r--app/assets/stylesheets/pages/todos.scss17
-rw-r--r--app/assets/stylesheets/pages/tree.scss4
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb17
-rw-r--r--app/controllers/dashboard_controller.rb25
-rw-r--r--app/controllers/projects/badges_controller.rb13
-rw-r--r--app/controllers/projects/branches_controller.rb21
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rw-r--r--app/controllers/projects/labels_controller.rb7
-rw-r--r--app/controllers/projects/milestones_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/finders/projects_finder.rb27
-rw-r--r--app/helpers/application_helper.rb20
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb17
-rw-r--r--app/helpers/issues_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb19
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/todos_helper.rb11
-rw-r--r--app/models/ability.rb51
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/milestoneish.rb20
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/issue.rb37
-rw-r--r--app/models/merge_request.rb11
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/project.rb11
-rw-r--r--app/models/repository.rb54
-rw-r--r--app/models/todo.rb32
-rw-r--r--app/models/user.rb13
-rw-r--r--app/services/commits/revert_service.rb7
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb15
-rw-r--r--app/services/projects/autocomplete_service.rb6
-rw-r--r--app/services/projects/housekeeping_service.rb2
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/services/search/project_service.rb3
-rw-r--r--app/services/system_note_service.rb12
-rw-r--r--app/services/todo_service.rb65
-rw-r--r--app/views/admin/users/_form.html.haml8
-rw-r--r--app/views/admin/users/index.html.haml10
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml4
-rw-r--r--app/views/dashboard/todos/_todo.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml14
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/_builds_settings.html.haml60
-rw-r--r--app/views/projects/diffs/_file.html.haml22
-rw-r--r--app/views/projects/edit.html.haml65
-rw-r--r--app/views/projects/issues/_issue.html.haml7
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml15
-rw-r--r--app/views/projects/issues/show.html.haml15
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml5
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml13
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml10
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml1
-rw-r--r--app/views/shared/groups/_group.html.haml13
-rw-r--r--app/views/shared/issuable/_filter.html.haml67
-rw-r--r--app/views/shared/issuable/_form.html.haml9
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml39
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml16
-rw-r--r--app/views/shared/issuable/_participants.html.haml16
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml48
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml4
-rw-r--r--app/views/shared/milestones/_summary.html.haml8
-rw-r--r--app/views/shared/milestones/_tabs.html.haml4
-rw-r--r--app/views/shared/milestones/_top.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml34
-rw-r--r--app/views/shared/snippets/_snippet.html.haml4
-rw-r--r--app/workers/gitlab_shell_one_shot_worker.rb10
-rw-r--r--app/workers/post_receive.rb46
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20160223192159_add_confidential_to_issues.rb6
-rw-r--r--db/migrate/20160310185910_add_external_flag_to_users.rb5
-rw-r--r--db/migrate/20160316192622_change_target_id_to_null_on_todos.rb5
-rw-r--r--db/migrate/20160316204731_add_commit_id_to_todos.rb6
-rw-r--r--db/schema.rb9
-rw-r--r--doc/README.md39
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/ci/deployment/README.md2
-rw-r--r--doc/ci/yaml/README.md112
-rw-r--r--doc/hooks/custom_hooks.md2
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/permissions/permissions.md21
-rw-r--r--doc/release/security.md2
-rw-r--r--doc/security/two_factor_authentication.md2
-rw-r--r--doc/update/8.2-to-8.3.md9
-rw-r--r--doc/update/8.3-to-8.4.md9
-rw-r--r--doc/update/8.4-to-8.5.md9
-rw-r--r--doc/update/8.5-to-8.6.md9
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/protected_branches.md2
-rw-r--r--features/project/issues/award_emoji.feature11
-rw-r--r--features/project/merge_requests.feature8
-rw-r--r--features/steps/dashboard/todos.rb1
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/project/badges/build.rb2
-rw-r--r--features/steps/project/issues/award_emoji.rb4
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/users.rb8
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb5
-rw-r--r--lib/gitlab/diff/file.rb4
-rw-r--r--lib/gitlab/git_post_receive.rb60
-rw-r--r--lib/gitlab/project_search_results.rb3
-rw-r--r--lib/gitlab/search_results.rb7
-rw-r--r--lib/support/nginx/gitlab11
-rw-r--r--lib/support/nginx/gitlab-ssl10
-rw-r--r--public/404.html44
-rw-r--r--public/422.html45
-rw-r--r--public/500.html44
-rw-r--r--public/502.html44
-rw-r--r--public/deploy.html41
-rw-r--r--public/logo.svg9
-rw-r--r--public/static.css36
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb96
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb162
-rw-r--r--spec/factories/issues.rb4
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/todos.rb10
-rw-r--r--spec/features/issues/new_branch_button_spec.rb49
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb2
-rw-r--r--spec/features/security/project/internal_access_spec.rb57
-rw-r--r--spec/features/security/project/private_access_spec.rb52
-rw-r--r--spec/features/security/project/public_access_spec.rb41
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb72
-rw-r--r--spec/lib/ci/status_spec.rb23
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb1
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb14
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb69
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/search_results_spec.rb91
-rw-r--r--spec/models/commit_spec.rb13
-rw-r--r--spec/models/concerns/mentionable_spec.rb5
-rw-r--r--spec/models/concerns/milestoneish_spec.rb104
-rw-r--r--spec/models/event_spec.rb36
-rw-r--r--spec/models/issue_spec.rb17
-rw-r--r--spec/models/merge_request_spec.rb48
-rw-r--r--spec/models/milestone_spec.rb20
-rw-r--r--spec/models/repository_spec.rb111
-rw-r--r--spec/models/todo_spec.rb85
-rw-r--r--spec/models/user_spec.rb15
-rw-r--r--spec/requests/api/issues_spec.rb112
-rw-r--r--spec/requests/api/users_spec.rb27
-rw-r--r--spec/services/git_push_service_spec.rb13
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb79
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb12
-rw-r--r--spec/services/todo_service_spec.rb9
-rw-r--r--spec/support/matchers/access_matchers.rb2
-rw-r--r--spec/support/mentionable_shared_examples.rb2
193 files changed, 3052 insertions, 1014 deletions
diff --git a/CHANGELOG b/CHANGELOG
index efb2709d839..000c8b8b8eb 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,9 +1,11 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
+ - Add confidential issues
- Bump gitlab_git to 9.0.3 (Stan Hu)
- Support Golang subpackage fetching (Stan Hu)
- Bump Capybara gem to 2.6.2 (Stan Hu)
+ - New branch button appears on issues where applicable
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
@@ -13,6 +15,7 @@ v 8.6.0 (unreleased)
- Add support for wiki with UTF-8 page names (Hiroyuki Sato)
- Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room
+ - HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
@@ -25,6 +28,7 @@ v 8.6.0 (unreleased)
- Rewrite logo to simplify SVG code (Sean Lang)
- Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
- Ignore jobs that start with `.` (hidden jobs)
+ - Hide builds from project's settings when the feature is disabled
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance
- Add support for cross-project label references
@@ -36,14 +40,23 @@ v 8.6.0 (unreleased)
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
+ - Fix empty source_sha on Merge Request when there is no diff (Pierre de La Morinerie)
- Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
+ - Fix an issue when the target branch of a MR had been deleted
- Add main language of a project in the list of projects (Tiago Botelho)
+ - Add #upcoming filter to Milestone filter (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page
+ - Create external users which are excluded of internal and private projects unless access was explicitly granted
- Continue parameters are checked to ensure redirection goes to the same instance
- User deletion is now done in the background so the request can not time out
+ - Canceled builds are now ignored in compound build status if marked as `allowed to fail`
+ - Trigger a todo for mentions on commits page
+
+v 8.5.8
+ - Bump Git version requirement to 2.7.4
v 8.5.7
- Bump Git version requirement to 2.7.3
@@ -56,7 +69,6 @@ v 8.5.5
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
- - Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4
- Do not cache requests for badges (including builds badge)
diff --git a/Gemfile b/Gemfile
index a3fb6779e9a..e500bfb7885 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 9.0'
+gem "gitlab_git", '~> 10.0'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b0dd83da52..63ed9441c62 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -359,11 +359,11 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (9.0.3)
+ gitlab_git (10.0.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
- rugged (~> 0.24.0b13)
+ rugged (~> 0.24.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
@@ -942,7 +942,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0)
- gitlab_git (~> 9.0)
+ gitlab_git (~> 10.0)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
diff --git a/README.md b/README.md
index 208427fcf8c..afa60116ebb 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1
-- Git 2.7.3+
+- Git 2.7.4+
- Redis 2.8+
- MySQL or PostgreSQL
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 1be86e3b820..f5e1ca9860d 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -14,7 +14,6 @@ class Dispatcher
path = page.split(':')
shortcut_handler = null
-
switch page
when 'projects:issues:index'
Issues.init()
@@ -25,6 +24,8 @@ class Dispatcher
new ZenMode()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
+ when 'dashboard:todos:index'
+ new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 4f038477755..c81e8bf760a 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -246,11 +246,15 @@ class GitLabDropdown
if oldValue
value = "#{oldValue},#{value}"
else
- @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
# Toggle active class for the tick mark
el.toggleClass "is-active"
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
+
if value?
if !field.length
# Create hidden input for form
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index d663e34871c..f50df1f5ea3 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -7,6 +7,7 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
@fixAffixScroll()
+ @initParticipants()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
@@ -84,3 +85,27 @@ class @Issue
type: 'PATCH'
url: $('form.js-issuable-update').attr('action')
data: patchData
+
+ initParticipants: ->
+ _this = @
+ $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+ $(".js-participants-author").each (i) ->
+ if i >= _this.PARTICIPANTS_ROW_COUNT
+ $(@)
+ .addClass "js-participants-hidden"
+ .hide()
+
+ toggleHiddenParticipants: (e) ->
+ e.preventDefault()
+
+ currentText = $(this).text().trim()
+ lessText = $(this).data("less-text")
+ originalText = $(this).data("original-text")
+
+ if currentText is originalText
+ $(this).text(lessText)
+ else
+ $(this).text(originalText)
+
+ $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index a0acf3028bf..1127b289264 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -41,24 +41,28 @@
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
- @timer = setTimeout(Issues.filterResults, 500)
+ @timer = setTimeout( ->
+ Issues.filterResults $("#issue_search_form")
+ , 500)
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
+ filterResults: (form) =>
+ $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
+ formAction = form.attr('action')
+ formData = form.serialize()
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
+ issuesUrl += formData
$.ajax
type: "GET"
- url: form.attr('action')
- data: form.serialize()
+ url: formAction
+ data: formData
complete: ->
- $('.issues-holder').css("opacity", '1.0')
+ $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
- $('.issues-holder').html(data.html)
+ $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 5ade2cb66cb..4a0c18a99a6 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -1,30 +1,32 @@
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
- projectId = $(dropdown).data('project-id')
- labelUrl = $(dropdown).data("labels")
- selectedLabel = $(dropdown).data('selected')
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ selectedLabel = $dropdown.data('selected')
if selectedLabel
- selectedLabel = selectedLabel.split(",")
+ selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
- showNo = $(dropdown).data('show-no')
- showAny = $(dropdown).data('show-any')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ defaultLabel = $dropdown.data('default-label')
if newLabelField.length
- $('.suggest-colors-dropdown a').on "click", (e) ->
+ $('.suggest-colors-dropdown a').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
- newColorField.val $(this).data("color")
+ newColorField.val $(this).data('color')
$('.js-dropdown-label-color-preview')
- .css 'background-color', $(this).data("color")
+ .css 'background-color', $(this).data('color')
.addClass 'is-active'
- $('.js-new-label-btn').on "click", (e) ->
+ $('.js-new-label-btn').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
- if newLabelField.val() isnt "" && newColorField.val() isnt ""
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
$('.js-new-label-btn').disable()
# Create new label with API
@@ -33,46 +35,38 @@ class @LabelsSelect
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
- $('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
- $(dropdown).glDropdown(
+ $dropdown.glDropdown(
data: (term, callback) ->
- # We have to fetch the JS version of the labels list because there is no
- # public facing JSON url for labels
$.ajax(
url: labelUrl
).done (data) ->
- html = $(data)
- data = []
- html.find('.label-row a').each ->
- data.push(
- title: $(@).text().trim()
- )
-
if showNo
data.unshift(
- id: "0"
- title: 'No label'
+ id: 0
+ title: 'No Label'
)
if showAny
data.unshift(
- title: 'Any label'
+ isAny: true
+ title: 'Any Label'
)
if data.length > 2
- data.splice 2, 0, "divider"
+ data.splice 2, 0, 'divider'
callback data
renderRow: (label) ->
if $.isArray(selectedLabel)
- selected = ""
+ selected = ''
$.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim()
- if selected is "" && label.title is selectedLbl
- selected = "is-active"
+ if selected is '' and label.title is selectedLbl
+ selected = 'is-active'
else
- selected = if label.title is selectedLabel then "is-active" else ""
+ selected = if label.title is selectedLabel then 'is-active' else ''
"<li>
<a href='#' class='#{selected}'>
@@ -83,10 +77,24 @@ class @LabelsSelect
search:
fields: ['title']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
id: (label) ->
- label.title
+ if label.isAny?
+ ''
+ else
+ label.title
clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
)
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 5e884454a65..e17a1adb648 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -1,60 +1,65 @@
class @MilestoneSelect
constructor: ->
$('.js-milestone-select').each (i, dropdown) ->
- projectId = $(dropdown).data('project-id')
- milestonesUrl = $(dropdown).data('milestones')
- selectedMilestone = $(dropdown).data('selected')
- showNo = $(dropdown).data('show-no')
- showAny = $(dropdown).data('show-any')
- useId = $(dropdown).data('use-id')
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ milestonesUrl = $dropdown.data('milestones')
+ selectedMilestone = $dropdown.data('selected')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ useId = $dropdown.data('use-id')
+ defaultLabel = $dropdown.data('default-label')
- $(dropdown).glDropdown(
+ $dropdown.glDropdown(
data: (term, callback) ->
$.ajax(
url: milestonesUrl
).done (data) ->
- html = $(data)
- data = []
- html.find('.milestone strong a').each ->
- link = $(@).attr("href").split("/")
- data.push(
- id: link[link.length - 1]
- title: $(@).text().trim()
- )
-
if showNo
data.unshift(
- id: "0"
+ id: '0'
title: 'No Milestone'
)
if showAny
data.unshift(
+ isAny: true
title: 'Any Milestone'
)
if data.length > 2
- data.splice 2, 0, "divider"
+ data.splice 2, 0, 'divider'
callback(data)
filterable: true
search:
fields: ['title']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
- if milestone.title isnt "Any milestone"
+ if !milestone.isAny?
milestone.title
else
- ""
+ ''
else
milestone.id
isSelected: (milestone) ->
milestone.title is selectedMilestone
clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
)
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 75d7f52bbb6..82532216589 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -343,6 +343,7 @@ class @Notes
updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html)
+ $('.js-timeago', $html).timeago()
$html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable')
@@ -626,10 +627,10 @@ class @Notes
if closebtn.text() isnt closetext
closebtn.text(closetext)
- if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ if reopenbtn.is('.btn-comment-and-reopen')
reopenbtn.removeClass('btn-comment-and-reopen')
- if closebtn.is(':not(.btn-comment-and-close)')
+ if closebtn.is('.btn-comment-and-close')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index fecdb9fc2e7..63dee4ed5d7 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -3,3 +3,16 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide()
$('.save-project-loader').show()
+ @toggleSettings()
+ @toggleSettingsOnclick()
+
+
+ toggleSettings: ->
+ checked = $("#project_builds_enabled").prop("checked")
+ if checked
+ $('.builds-feature').show()
+ else
+ $('.builds-feature').hide()
+
+ toggleSettingsOnclick: ->
+ $("#project_builds_enabled").on 'click', @toggleSettings
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..b6b4bd90e6a
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,56 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+
+ doneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ @clearDone $this.closest('li')
+ @updateBadges data
+
+ allDoneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ $this.remove()
+ $('.js-todos-list').remove()
+ @updateBadges data
+
+ clearDone: ($row) ->
+ $ul = $row.closest('ul')
+ $row.remove()
+
+ if not $ul.find('li').length
+ $ul.parents('.panel').remove()
+
+ updateBadges: (data) ->
+ $('.todos-pending .badge, .todos-pending-count').text data.count
+ $('.todos-done .badge').text data.done_count
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 987c6f4b8d2..3d6452d2f46 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -4,14 +4,16 @@ class @UsersSelect
@userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) =>
- @projectId = $(dropdown).data('project-id')
- @showCurrentUser = $(dropdown).data('current-user')
- showNullUser = $(dropdown).data('null-user')
- showAnyUser = $(dropdown).data('any-user')
- firstUser = $(dropdown).data('first-user')
- selectedId = $(dropdown).data('selected')
-
- $(dropdown).glDropdown(
+ $dropdown = $(dropdown)
+ @projectId = $dropdown.data('project-id')
+ @showCurrentUser = $dropdown.data('current-user')
+ showNullUser = $dropdown.data('null-user')
+ showAnyUser = $dropdown.data('any-user')
+ firstUser = $dropdown.data('first-user')
+ selectedId = $dropdown.data('selected')
+ defaultLabel = $dropdown.data('default-label')
+
+ $dropdown.glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
@@ -52,10 +54,21 @@ class @UsersSelect
search:
fields: ['name', 'username']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ fieldName: $dropdown.data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 90c3ce0e84c..c36f29dda0e 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -28,10 +28,6 @@
border-bottom: 1px solid $border-color;
color: $gl-gray;
- a {
- color: $md-link-color;
- }
-
&.oneline-block {
line-height: 42px;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index fa115a4bf56..657c5f033c7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -208,3 +208,13 @@
background-color: #e4e7ed !important;
}
}
+
+.btn-loading {
+ &:not(.disabled) .fa {
+ display: none;
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 180926b3b97..bc03c2180be 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -8,20 +8,20 @@
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top:10px }
+.prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top:20px }
-.prepend-left-10 { margin-left:10px }
+.prepend-top-20 { margin-top: 20px }
+.prepend-left-10 { margin-left: 10px }
.prepend-left-default { margin-left: $gl-padding; }
-.prepend-left-20 { margin-left:20px }
+.prepend-left-20 { margin-left: 20px }
.append-right-5 { margin-right: 5px }
-.append-right-10 { margin-right:10px }
+.append-right-10 { margin-right: 10px }
.append-right-default { margin-right: $gl-padding; }
-.append-right-20 { margin-right:20px }
-.append-bottom-0 { margin-bottom:0 }
-.append-bottom-10 { margin-bottom:10px }
-.append-bottom-15 { margin-bottom:15px }
-.append-bottom-20 { margin-bottom:20px }
+.append-right-20 { margin-right: 20px }
+.append-bottom-0 { margin-bottom: 0 }
+.append-bottom-10 { margin-bottom: 10px }
+.append-bottom-15 { margin-bottom: 15px }
+.append-bottom-20 { margin-bottom: 20px }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block }
.center { text-align: center }
@@ -134,10 +134,10 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
- img { max-width:100% }
+ img { max-width: 100% }
.note-title {
li {
- border-bottom:none !important;
+ border-bottom: none !important;
}
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 3197ea84460..a48b6c17fa0 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -9,6 +9,12 @@
border-left: $caret-width-base solid transparent;
}
+.btn-group {
+ .caret {
+ margin-left: 0;
+ }
+}
+
.dropdown {
position: relative;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index c431e2b0df3..40a508c1ebc 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -3,22 +3,11 @@
vertical-align: top;
}
-@media (min-width: 800px) {
+@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
- select, .select2-container {
- width: 120px !important;
- display: inline-block;
- }
- }
-}
-
-@media (min-width: 1200px) {
- .issues-filters,
- .issues_bulk_update {
- select, .select2-container {
- width: 150px !important;
- display: inline-block;
+ .dropdown-menu-toggle {
+ width: 132px;
}
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index bfec0911b3c..b17c8bcbb1e 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -111,14 +111,17 @@ ul.content-list {
> li {
border-color: $table-border-color;
- color: $list-text-color;
font-size: $list-font-size;
+ color: $list-text-color;
.title {
- color: $list-title-color;
font-weight: 600;
}
+ a {
+ color: $gl-dark-link-color;
+ }
+
.description {
p {
@include str-truncated;
@@ -141,6 +144,10 @@ ul.content-list {
}
}
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
+}
+
ul.controls {
padding-top: 1px;
float: right;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 5e3546bc6ff..be626678bd7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,45 +1,75 @@
-$row-hover: #f4f8fe;
-$gl-text-color: #54565b;
-$gl-text-green: #4a2;
-$gl-text-red: #d12f19;
-$gl-text-orange: #d90;
-$gl-header-color: #323232;
-$gl-link-color: #333c48;
-$md-text-color: #444;
-$md-link-color: #3084bb;
-$progress-color: #c0392b;
-$gl-font-size: 15px;
-$list-font-size: 15px;
+/*
+ * Layout
+ */
$sidebar_collapsed_width: 62px;
$sidebar_width: 230px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
-$avatar_radius: 50%;
+
+/*
+ * UI elements
+ */
+$border-color: #efeff1;
+$table-border-color: #eef0f2;
+$background-color: #faf9f9;
+
+/*
+ * Text
+ */
+$gl-font-size: 15px;
+$gl-title-color: #333;
+$gl-text-color: #555;
+$gl-text-green: #4a2;
+$gl-text-red: #d12f19;
+$gl-text-orange: #d90;
+$gl-link-color: #3084bb;
+$gl-dark-link-color: #333;
+$gl-placeholder-color: #8f8f8f;
+$gl-gray: $gl-text-color;
+$gl-header-color: $gl-title-color;
+
+/*
+ * Lists
+ */
+$list-font-size: $gl-font-size;
+$list-title-color: $gl-title-color;
+$list-text-color: $gl-text-color;
+
+/*
+ * Markdown
+ */
+$md-text-color: $gl-text-color;
+$md-link-color: $gl-link-color;
+
+/*
+ * Code
+ */
$code_font_size: 13px;
$code_line_height: 1.5;
-$border-color: #efeff1;
-$table-border-color: #eef0f2;
-$background-color: #faf9f9;
-$header-height: 58px;
-$fixed-layout-width: 1280px;
-$gl-gray: #5a5a5a;
+
+/*
+ * Padding
+ */
$gl-padding: 16px;
$gl-btn-padding: 10px;
$gl-vert-padding: 6px;
-$gl-padding-top:10px;
+$gl-padding-top: 10px;
+
+/*
+ * Misc
+ */
+$row-hover: #f4f8fe;
+$progress-color: #c0392b;
+$avatar_radius: 50%;
+$header-height: 58px;
+$fixed-layout-width: 1280px;
$gl-avatar-size: 40px;
-$secondary-text: #7f8fa4;
$error-exclamation-point: #e62958;
$border-radius-default: 3px;
-$list-title-color: #333;
-$list-text-color: #555;
-
$btn-transparent-color: #8f8f8f;
-
$ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px;
-
$provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1;
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss
index e2070f17c3b..878f44116ba 100644
--- a/app/assets/stylesheets/pages/appearances.scss
+++ b/app/assets/stylesheets/pages/appearances.scss
@@ -4,7 +4,7 @@
}
.appearance-light-logo-preview {
- background-color: $background-color;
+ background-color: $background-color;
max-width: 72px;
padding: 10px;
margin-bottom: 10px;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index d57be1b2daa..33b3c7558ed 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -55,7 +55,7 @@ li.commit {
}
.commit-row-message {
- color: $gl-link-color;
+ color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 88639399148..cf7567513ec 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -11,15 +11,15 @@
}
.dashboard-search-filter {
- padding:5px;
+ padding: 5px;
.search-text-input {
- float:left;
+ float: left;
@extend .col-md-2;
}
.btn {
margin-left: 5px;
- float:left;
+ float: left;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index db06b8288c2..d5862a11aca 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,7 +1,7 @@
// Common
.diff-file {
border: 1px solid $border-color;
- border-top: none;
+ margin-bottom: $gl-padding;
.diff-header {
position: relative;
@@ -361,3 +361,11 @@
border-color: $border;
}
}
+
+.files {
+ margin-top: -1px;
+
+ .diff-file:last-child {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index e7da0a2f689..84eefd01cfe 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -6,7 +6,7 @@
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
border-bottom: 1px solid $table-border-color;
- color: #7f8fa4;
+ color: $list-text-color;
&.event-inline {
.avatar {
@@ -21,7 +21,7 @@
}
a {
- color: #4c4e54;
+ color: $gl-dark-link-color;
}
.avatar {
@@ -31,10 +31,7 @@
.event-title {
@include str-truncated(calc(100% - 174px));
font-weight: 600;
-
- .author_name {
- color: #333;
- }
+ color: $list-text-color;
}
.event-body {
@@ -94,7 +91,7 @@
}
}
- &:last-child { border:none }
+ &:last-child { border: none }
.event_commits {
li {
@@ -138,7 +135,7 @@
@include str-truncated(100%);
padding: 5px 0;
font-size: 13px;
- float:left;
+ float: left;
margin-right: -150px;
padding-right: 150px;
line-height: 20px;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index faa2ebfda78..2760af8a48a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,34 +1,3 @@
-@media (max-width: $screen-sm-max) {
- .issuable-affix {
- margin-top: 20px;
- }
-}
-
-@media (max-width: $screen-md-max) {
- .issuable-affix {
- position: static;
- }
-}
-
-@media (min-width: $screen-md-max) {
- .issuable-affix {
- &.affix-top {
- position: static;
- }
-
- &.affix {
- position: fixed;
- top: 70px;
- margin-right: 35px;
-
- &.no-affix {
- position: relative;
- top: 0;
- }
- }
- }
-}
-
.issuable-details {
section {
.issuable-discussion {
@@ -54,20 +23,25 @@
padding: 6px 10px;
}
}
+
+ &.has-labels {
+ margin-bottom: -5px;
+ }
}
.issuable-sidebar {
.block {
@include clearfix;
- padding: $gl-padding 0;
+ padding: $gl-padding 0;
border-bottom: 1px solid $border-gray-light;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
// --
- &:first-child {
- padding-top: 5px;
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ padding-bottom: 10px;
}
&:last-child {
@@ -75,7 +49,6 @@
}
span {
- margin-top: 7px;
display: inline-block;
}
@@ -84,7 +57,7 @@
}
.issuable-count {
-
+ margin-top: 7px;
}
.gutter-toggle {
@@ -99,19 +72,19 @@
.title {
color: $gl-text-color;
- margin-bottom: 8px;
+ margin-bottom: 10px;
+ line-height: 1;
.avatar {
margin-left: 0;
}
- label {
- font-weight: normal;
- margin-right: 4px;
- }
-
.edit-link {
color: $gl-gray;
+
+ &:hover {
+ color: $md-link-color;
+ }
}
}
@@ -144,11 +117,6 @@
.btn-clipboard {
color: $gl-gray;
}
-
- .participants .avatar {
- margin-top: 6px;
- margin-right: 2px;
- }
}
.right-sidebar {
@@ -163,8 +131,12 @@
&.right-sidebar-expanded {
width: $gutter_width;
- hr {
- display: none;
+ .value {
+ line-height: 1;
+ }
+
+ .bold {
+ font-weight: 600;
}
.sidebar-collapsed-icon {
@@ -172,8 +144,23 @@
}
.gutter-toggle {
+ margin-top: 7px;
border-left: 1px solid $border-gray-light;
}
+
+ .assignee .avatar {
+ float: left;
+ margin-right: 10px;
+ margin-bottom: 0;
+ margin-left: 0;
+ }
+
+ .username {
+ display: block;
+ margin-top: 4px;
+ font-size: 13px;
+ font-weight: normal;
+ }
}
.subscribe-button {
@@ -193,14 +180,6 @@
width: $sidebar_collapsed_width;
padding-top: 0;
- hr {
- margin: 0;
- color: $gray-normal;
- border-color: $gray-normal;
- width: 62px;
- margin-left: -20px
- }
-
.block {
width: $sidebar_collapsed_width - 1px;
margin-left: -19px;
@@ -209,12 +188,18 @@
overflow: hidden;
}
+ .participants {
+ border-bottom: 1px solid $border-gray-light;
+ }
+
.hide-collapsed {
display: none;
}
.gutter-toggle {
- margin-left: -36px;
+ width: 100%;
+ margin-left: 0;
+ padding-left: 25px;
}
.sidebar-collapsed-icon {
@@ -229,6 +214,10 @@
margin-top: 0;
}
+ .author {
+ display: none;
+ }
+
.btn-clipboard {
border: none;
@@ -241,6 +230,11 @@
}
}
}
+
+ .sidebar-collapsed-user {
+ padding-bottom: 0;
+ margin-bottom: 10px;
+ }
}
.btn {
@@ -251,6 +245,13 @@
border: 1px solid $border-gray-dark;
}
}
+
+ a:not(.btn) {
+ &:hover {
+ color: $md-link-color;
+ text-decoration: none;
+ }
+ }
}
.btn-default.gutter-toggle {
@@ -262,3 +263,37 @@
color: $gray-darkest;
}
}
+
+.edited-text {
+ color: $gray-darkest;
+
+ .author_link {
+ color: $gray-darkest;
+ }
+}
+
+.participants-list {
+ margin: -5px -5px;
+}
+
+.participants-author {
+ display: inline-block;
+ padding: 5px 5px;
+
+ .author_link {
+ display: block;
+ }
+
+ .avatar.avatar-inline {
+ margin: 0;
+ }
+}
+
+.participants-more {
+ margin-top: 5px;
+ margin-left: 5px;
+
+ a {
+ color: #8c8c8c;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 73718ff511a..6a1d28590c2 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -3,7 +3,7 @@
padding: 10px $gl-padding;
position: relative;
- .issue-title {
+ .title {
margin-bottom: 2px;
}
@@ -49,7 +49,7 @@ form.edit-issue {
margin: 0;
}
-.merge-requests-title {
+.merge-requests-title, .related-branches-title {
font-size: 16px;
font-weight: 600;
}
@@ -130,14 +130,14 @@ form.edit-issue {
}
.issue-closed-by-widget {
- color: $secondary-text;
+ color: $gl-text-color;
margin-left: 52px;
}
.editor-details {
display: block;
-
+
@media (min-width: $screen-sm-min) {
display: inline-block;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index d9c47881265..bc41f7d306f 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -28,7 +28,7 @@
img {
max-width: 100%;
- margin-bottom: 30px;
+ margin-bottom: 30px;
}
a {
@@ -85,7 +85,7 @@
&.middle {
border-top: 0;
- margin-bottom:0;
+ margin-bottom: 0;
@include border-radius(0);
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 969c79a9be9..d408853cc80 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,9 @@
*/
@-webkit-keyframes targe3-note {
- from { background:#fffff0; }
- 50% { background:#ffffd3; }
- to { background:#fffff0; }
+ from { background: #fffff0; }
+ 50% { background: #ffffd3; }
+ to { background: #fffff0; }
}
ul.notes {
@@ -93,12 +93,12 @@ ul.notes {
.discussion {
overflow: hidden;
display: block;
- position:relative;
+ position: relative;
}
.note {
display: block;
- position:relative;
+ position: relative;
.note-body {
overflow: auto;
@@ -108,6 +108,13 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // On diffs code should wrap nicely and not overflow
+ pre {
+ code {
+ white-space: pre-wrap;
+ }
+ }
+
// Reset ul style types since we're nested inside a ul already
& > ul {
list-style-type: disc;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 3fe2c9a3346..82c5069638d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -33,6 +33,13 @@
.project-settings-dropdown {
margin-left: 10px;
display: inline-block;
+
+ .dropdown-menu {
+ left: auto;
+ width: auto;
+ right: 0px;
+ max-width: 240px;
+ }
}
}
@@ -286,11 +293,11 @@ table.table.protected-branches-list tr.no-border {
padding-bottom: 4px;
ul.nav {
- display:inline-block;
+ display: inline-block;
}
.nav li {
- display:inline;
+ display: inline;
}
.nav > li > a {
@@ -303,11 +310,11 @@ table.table.protected-branches-list tr.no-border {
}
li {
- display:inline;
+ display: inline;
}
a {
- float:left;
+ float: left;
font-size: 17px;
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 27970eba159..f983e9829e6 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -14,25 +14,8 @@
}
.todo-item {
- font-size: $gl-font-size;
- padding-left: $gl-avatar-size + $gl-padding-top;
- color: $secondary-text;
-
- a {
- color: #4c4e54;
- }
-
- .avatar {
- margin-left: -($gl-avatar-size + $gl-padding-top);
- }
-
.todo-title {
@include str-truncated(calc(100% - 174px));
- font-weight: 600;
-
- .author-name {
- color: #333;
- }
}
.todo-body {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index ef63b010600..25b5e95583e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -41,12 +41,12 @@
vertical-align: middle;
i, a {
- color: $gl-link-color;
+ color: $gl-dark-link-color;
}
img {
position: relative;
- top:-1px;
+ top: -1px;
}
}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 3063d299b1a..9abf08d0e19 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController
:email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id
+ :projects_limit, :can_create_group, :admin, :key_id, :external
)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 43cf8fa71af..be488483b09 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,25 +1,34 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- before_action :find_todos, only: [:index, :destroy_all]
+ before_action :find_todos, only: [:index, :destroy, :destroy_all]
def index
@todos = @todos.page(params[:page]).per(PER_PAGE)
end
def destroy
- todo.done!
+ todo.done
+
+ todo_notice = 'Todo was successfully marked as done.'
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
+ format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { render nothing: true }
+ format.json do
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
def destroy_all
- @todos.each(&:done!)
+ @todos.each(&:done)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { render nothing: true }
+ format.json do
+ find_todos
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 139e40db180..b538c7d1608 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
include MergeRequestsAction
before_action :event_filter, only: :activity
- before_action :projects, only: [:issues, :merge_requests]
+ before_action :projects, only: [:issues, :merge_requests, :labels, :milestones]
respond_to :html
@@ -20,6 +20,29 @@ class DashboardController < Dashboard::ApplicationController
end
end
+ def labels
+ labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title)
+
+ respond_to do |format|
+ format.json do
+ render json: labels
+ end
+ end
+ end
+
+ def milestones
+ milestones = Milestone.where(project_id: @projects).active
+ epoch = DateTime.parse('1970-01-01')
+ grouped_milestones = GlobalMilestone.build_collection(milestones)
+ grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
+
+ respond_to do |format|
+ format.json do
+ render json: grouped_milestones
+ end
+ end
+ end
+
protected
def load_events
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index dc9c96df003..6ff47c4033a 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -1,5 +1,5 @@
class Projects::BadgesController < Projects::ApplicationController
- before_action :set_no_cache
+ before_action :no_cache_headers
def build
respond_to do |format|
@@ -10,15 +10,4 @@ class Projects::BadgesController < Projects::ApplicationController
end
end
end
-
- private
-
- def set_no_cache
- expires_now
-
- # Add some deprecated headers for older agents
- #
- response.headers['Pragma'] = 'no-cache'
- response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
- end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 4db3b3bf23d..43ea717cbd2 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
-
+
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
@@ -23,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController
def create
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- ref = sanitize(strip_tags(params[:ref]))
- ref = Addressable::URI.unescape(ref)
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
+ if params[:issue_iid]
+ issue = @project.issues.find_by(iid: params[:issue_iid])
+ SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
+ end
+
if result[:status] == :success
@branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project,
@@ -49,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController
format.js { render status: status[:return_code] }
end
end
+
+ private
+
+ def ref
+ if params[:ref]
+ ref_escaped = sanitize(strip_tags(params[:ref]))
+ Addressable::URI.unescape(ref_escaped)
+ else
+ @project.default_branch
+ end
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b0a03ee45cc..6603f28a082 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :issue, only: [:edit, :update, :show]
# Allow read any issue
- before_action :authorize_read_issue!
+ before_action :authorize_read_issue!, only: [:show]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -65,6 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
@merge_requests = @issue.referenced_merge_requests(current_user)
+ @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue)
end
@@ -127,6 +128,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
alias_method :subscribable_resource, :issue
+ def authorize_read_issue!
+ return render_404 unless can?(current_user, :read_issue, @issue)
+ end
+
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
end
@@ -157,7 +162,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
- :title, :assignee_id, :position, :description,
+ :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 40d8098690a..5f471d405f5 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -12,6 +12,13 @@ class Projects::LabelsController < Projects::ApplicationController
def index
@labels = @project.labels.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @project.labels
+ end
+ end
end
def new
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index da46731d945..0998b191c07 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -19,7 +19,15 @@ class Projects::MilestonesController < Projects::ApplicationController
end
@milestones = @milestones.includes(:project)
- @milestones = @milestones.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html do
+ @milestones = @milestones.page(params[:page]).per(PER_PAGE)
+ end
+ format.json do
+ render json: @milestones
+ end
+ end
end
def new
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 36f37221c58..c9930480770 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -134,7 +134,7 @@ class ProjectsController < ApplicationController
def autocomplete_sources
note_type = params['type']
note_id = params['type_id']
- autocomplete = ::Projects::AutocompleteService.new(@project)
+ autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 20a2b0ce8f0..c2befa5a5b3 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder
def klass
Issue
end
+
+ private
+
+ def init_collection
+ Issue.visible_to_user(current_user)
+ end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 2c55f088594..3a5fc5b5907 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -40,25 +40,26 @@ class ProjectsFinder
private
def group_projects(current_user, group)
- if current_user
- [
- group_projects_for_user(current_user, group),
- group.projects.public_and_internal_only,
- group.shared_projects.visible_to_user(current_user)
- ]
+ return [group.projects.public_only] unless current_user
+
+ user_group_projects = [
+ group_projects_for_user(current_user, group),
+ group.shared_projects.visible_to_user(current_user)
+ ]
+ if current_user.external?
+ user_group_projects << group.projects.public_only
else
- [group.projects.public_only]
+ user_group_projects << group.projects.public_and_internal_only
end
end
def all_projects(current_user)
- if current_user
- [
- current_user.authorized_projects,
- public_and_internal_projects
- ]
+ return [public_projects] unless current_user
+
+ if current_user.external?
+ [current_user.authorized_projects, public_projects]
else
- [Project.public_only]
+ [current_user.authorized_projects, public_and_internal_projects]
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d1b1c61b710..e6ceb213532 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -182,7 +182,7 @@ module ApplicationHelper
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
- class: "#{html_class} js-timeago js-timeago-pending",
+ class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
datetime: time.to_time.getutc.iso8601,
title: time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
@@ -196,6 +196,22 @@ module ApplicationHelper
element
end
+ def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false)
+ return if object.updated_at == object.created_at
+
+ content_tag :small, class: "edited-text" do
+ output = content_tag(:span, "Edited ")
+ output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class)
+
+ if include_author && object.updated_by && object.updated_by != object.author
+ output << content_tag(:span, " by ")
+ output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil)
+ end
+
+ output
+ end
+ end
+
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.preserve(markdown(file_content))
@@ -285,7 +301,7 @@ module ApplicationHelper
if project.nil?
nil
elsif current_controller?(:issues)
- project.issues.send(entity).count
+ project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 74f326e0b83..ceff1fbb161 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -24,7 +24,7 @@ module DropdownsHelper
capture(&block) if block && !options.has_key?(:footer_content)
end
- if block && options.has_key?(:footer_content)
+ if block && options[:footer_content]
output << content_tag(:div, class: "dropdown-footer") do
capture(&block)
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 37a888d9c60..a67a6b208e2 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -194,7 +194,7 @@ module EventsHelper
end
def event_to_atom(xml, event)
- if event.proper?
+ if event.proper?(current_user)
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 2dfeddf7368..81df2094392 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -20,6 +20,23 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end
+ def user_dropdown_label(user_id, default_label)
+ return "Unassigned" if user_id == "0"
+
+ if @project
+ member = @project.team.find_member(user_id)
+ user = member.user if member
+ else
+ user = User.find_by(id: user_id)
+ end
+
+ if user
+ user.name
+ else
+ default_label
+ end
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index ae4ebc0854a..e00d3204027 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -98,6 +98,10 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
+ def confidential_icon(issue)
+ icon('eye-slash') if issue.confidential?
+ end
+
def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name) rescue ""
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 007cc951ed1..ed37176aa6b 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -110,19 +110,12 @@ module LabelsHelper
end
end
- def projects_labels_options
- labels =
- if @project
- @project.labels
- else
- Label.where(project_id: @projects)
- end
-
- grouped_labels = GlobalLabel.build_collection(labels)
- grouped_labels.unshift(Label::None)
- grouped_labels.unshift(Label::Any)
-
- options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
+ def labels_filter_path
+ if @project
+ namespace_project_labels_path(@project.namespace, @project, :json)
+ else
+ labels_dashboard_path(:json)
+ end
end
def label_subscription_status(label)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e8ac8788d9d..c9d8787bd19 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -38,7 +38,7 @@ module MilestonesHelper
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
- style: "width: #{milestone.percent_complete}%;"
+ style: "width: #{milestone.percent_complete(current_user)}%;"
}
content_tag :div, class: 'progress' do
@@ -46,22 +46,12 @@ module MilestonesHelper
end
end
- def projects_milestones_options
- milestones =
- if @project
- @project.milestones
- else
- Milestone.where(project_id: @projects)
- end.active
-
- epoch = DateTime.parse('1970-01-01')
- grouped_milestones = GlobalMilestone.build_collection(milestones)
- grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- grouped_milestones.unshift(Milestone::None)
- grouped_milestones.unshift(Milestone::Any)
- grouped_milestones.unshift(Milestone::Upcoming)
-
- options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
+ def milestones_filter_dropdown_path
+ if @project
+ namespace_project_milestones_path(@project.namespace, @project, :json)
+ else
+ milestones_dashboard_path(:json)
+ end
end
def milestone_remaining_days(milestone)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index b5acb80b720..5473419ef24 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -26,7 +26,7 @@ module ProjectsHelper
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
end
- def link_to_member(project, author, opts = {})
+ def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
@@ -44,6 +44,8 @@ module ProjectsHelper
author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
end
+ author_html << capture(&block) if block
+
author_html = author_html.html_safe
if opts[:name]
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 07ddc691d85..edc5686cf08 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -16,14 +16,19 @@ module TodosHelper
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
- link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) }
+ link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
end
def todo_target_path(todo)
anchor = dom_id(todo.note) if todo.note.present?
- polymorphic_path([todo.project.namespace.becomes(Namespace),
- todo.project, todo.target], anchor: anchor)
+ if todo.for_commit?
+ namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
+ todo.target, anchor: anchor)
+ else
+ polymorphic_path([todo.project.namespace.becomes(Namespace),
+ todo.project, todo.target], anchor: anchor)
+ end
end
def todos_filter_params
diff --git a/app/models/ability.rb b/app/models/ability.rb
index fe9e0aab717..e22da4806e6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -49,7 +49,6 @@ class Ability
rules = [
:read_project,
:read_wiki,
- :read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
@@ -63,6 +62,9 @@ class Ability
# Allow to read builds by anonymous user if guests are allowed
rules << :read_build if project.public_builds?
+ # Allow to read issues by anonymous user if issue is not confidential
+ rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
+
rules - project_disabled_features_rules(project)
else
[]
@@ -109,23 +111,10 @@ class Ability
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
- team = project.team
-
- # Rules based on role in project
- if team.master?(user)
- rules.push(*project_master_rules)
-
- elsif team.developer?(user)
- rules.push(*project_dev_rules)
-
- elsif team.reporter?(user)
- rules.push(*project_report_rules)
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- elsif team.guest?(user)
- rules.push(*project_guest_rules)
- end
-
- if project.public? || project.internal?
+ if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
# Allow to read builds for internal projects
@@ -148,6 +137,19 @@ class Ability
end
end
+ def project_team_rules(team, user)
+ # Rules based on role in project
+ if team.master?(user)
+ project_master_rules
+ elsif team.developer?(user)
+ project_dev_rules
+ elsif team.reporter?(user)
+ project_report_rules
+ elsif team.guest?(user)
+ project_guest_rules
+ end
+ end
+
def public_project_rules
@public_project_rules ||= project_guest_rules + [
:download_code,
@@ -321,6 +323,7 @@ class Ability
end
rules += project_abilities(user, subject.project)
+ rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules
end
end
@@ -356,7 +359,7 @@ class Ability
]
end
- if snippet.public? || snippet.internal?
+ if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet
end
@@ -439,5 +442,17 @@ class Ability
:"admin_#{name}"
]
end
+
+ 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)
+ rules.delete(:admin_issue)
+ rules.delete(:read_issue)
+ rules.delete(:update_issue)
+ end
+
+ rules
+ end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3b1aa0f5c80..3377a85a55a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -114,7 +114,7 @@ class CommitStatus < ActiveRecord::Base
end
def ignored?
- failed? && allow_failure?
+ allow_failure? && (failed? || canceled?)
end
def duration
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index d67df7c1d9c..5b8e3f654ea 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,18 +1,18 @@
module Milestoneish
- def closed_items_count
- issues.closed.size + merge_requests.closed_and_merged.size
+ def closed_items_count(user = nil)
+ issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end
- def total_items_count
- issues.size + merge_requests.size
+ def total_items_count(user = nil)
+ issues_visible_to_user(user).size + merge_requests.size
end
- def complete?
- total_items_count == closed_items_count
+ def complete?(user = nil)
+ total_items_count(user) == closed_items_count(user)
end
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
+ def percent_complete(user = nil)
+ ((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError
0
end
@@ -22,4 +22,8 @@ module Milestoneish
(due_date - Date.today).to_i
end
+
+ def issues_visible_to_user(user = nil)
+ issues.visible_to_user(user)
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 9a0bbf50f8b..a5cfeaf388e 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -73,15 +73,17 @@ class Event < ActiveRecord::Base
end
end
- def proper?
+ def proper?(user = nil)
if push?
true
elsif membership_changed?
true
elsif created_project?
true
+ elsif issue?
+ Ability.abilities.allowed?(user, :read_issue, issue)
else
- ((issue? || merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target) || milestone?
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5f58c0508fd..5347d4fa1be 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -58,6 +58,13 @@ class Issue < ActiveRecord::Base
attributes
end
+ def self.visible_to_user(user)
+ return where(confidential: 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))
+ end
+
def self.reference_prefix
'#'
end
@@ -87,11 +94,20 @@ class Issue < ActiveRecord::Base
end
def referenced_merge_requests(current_user = nil)
- Gitlab::ReferenceExtractor.lazily do
- [self, *notes].flat_map do |note|
- note.all_references(current_user).merge_requests
- end
- end.sort_by(&:iid)
+ @referenced_merge_requests ||= {}
+ @referenced_merge_requests[current_user] ||= begin
+ Gitlab::ReferenceExtractor.lazily do
+ [self, *notes].flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end
+ end.sort_by(&:iid).uniq
+ end
+ end
+
+ def related_branches
+ return [] if self.project.empty_repo?
+
+ self.project.repository.branch_names.select { |branch| branch.end_with?("-#{iid}") }
end
# Reset issue events cache
@@ -120,4 +136,15 @@ class Issue < ActiveRecord::Base
note.all_references(current_user).merge_requests
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+
+ def to_branch_name
+ "#{title.parameterize}-#{iid}"
+ end
+
+ def can_be_worked_on?(current_user)
+ !self.closed? &&
+ !self.project.forked? &&
+ self.related_branches.empty? &&
+ self.closed_by_merge_requests(current_user).empty?
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 188325045e2..a6140b5b04c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -516,11 +516,15 @@ class MergeRequest < ActiveRecord::Base
end
def target_sha
- @target_sha ||= target_project.repository.commit(target_branch).sha
+ @target_sha ||= target_project.repository.commit(target_branch).try(:sha)
end
def source_sha
- last_commit.try(:sha)
+ last_commit.try(:sha) || source_tip.try(:sha)
+ end
+
+ def source_tip
+ source_branch && source_project.repository.commit(source_branch)
end
def fetch_ref
@@ -568,8 +572,11 @@ class MergeRequest < ActiveRecord::Base
end
def compute_diverged_commits_count
+ return 0 unless source_sha && target_sha
+
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
end
+ private :compute_diverged_commits_count
def diverged_from_target_branch?
diverged_commits_count > 0
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 374590ba0c5..de7183bf6b4 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -121,8 +121,8 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero?
end
- def is_empty?
- total_items_count.zero?
+ def is_empty?(user = nil)
+ total_items_count(user).zero?
end
def author_id
diff --git a/app/models/project.rb b/app/models/project.rb
index 89a55a510cd..412c6c6732d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -254,12 +254,6 @@ class Project < ActiveRecord::Base
where('projects.last_activity_at < ?', 6.months.ago)
end
- def publicish(user)
- visibility_levels = [Project::PUBLIC]
- visibility_levels << Project::INTERNAL if user
- where(visibility_level: visibility_levels)
- end
-
def with_push
joins(:events).where('events.action = ?', Event::PUSHED)
end
@@ -577,10 +571,7 @@ class Project < ActiveRecord::Base
end
def avatar_in_git
- @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png')
- @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg')
- @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif')
- @avatar_file
+ repository.avatar
end
def avatar_url
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e555e97689d..25d24493f6e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -3,6 +3,10 @@ require 'securerandom'
class Repository
class CommitError < StandardError; end
+ # Files to use as a project avatar in case no avatar was uploaded via the web
+ # UI.
+ AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
+
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
@@ -223,12 +227,6 @@ class Repository
send(key)
end
end
-
- branches.each do |branch|
- unless cache.exist?(:"diverging_commit_counts_#{branch.name}")
- send(:diverging_commit_counts, branch)
- end
- end
end
def expire_tags_cache
@@ -241,12 +239,13 @@ class Repository
@branches = nil
end
- def expire_cache(branch_name = nil)
+ def expire_cache(branch_name = nil, revision = nil)
cache_keys.each do |key|
cache.expire(key)
end
expire_branch_cache(branch_name)
+ expire_avatar_cache(branch_name, revision)
# This ensures this particular cache is flushed after the first commit to a
# new repository.
@@ -296,18 +295,6 @@ class Repository
@tag_count = nil
end
- def rebuild_cache
- cache_keys.each do |key|
- cache.expire(key)
- send(key)
- end
-
- branches.each do |branch|
- cache.expire(:"diverging_commit_counts_#{branch.name}")
- diverging_commit_counts(branch)
- end
- end
-
def lookup_cache
@lookup_cache ||= {}
end
@@ -316,6 +303,23 @@ class Repository
cache.expire(:branch_names)
end
+ def expire_avatar_cache(branch_name = nil, revision = nil)
+ # Avatars are pulled from the default branch, thus if somebody pushes to a
+ # different branch there's no need to expire anything.
+ return if branch_name && branch_name != root_ref
+
+ # We don't want to flush the cache if the commit didn't actually make any
+ # changes to any of the possible avatar files.
+ if revision && commit = self.commit(revision)
+ return unless commit.diffs.
+ any? { |diff| AVATAR_FILES.include?(diff.new_path) }
+ end
+
+ cache.expire(:avatar)
+
+ @avatar = nil
+ end
+
# Runs code just before a repository is deleted.
def before_delete
expire_cache if exists?
@@ -350,8 +354,8 @@ class Repository
end
# Runs code after a new commit has been pushed.
- def after_push_commit(branch_name)
- expire_cache(branch_name)
+ def after_push_commit(branch_name, revision)
+ expire_cache(branch_name, revision)
end
# Runs code after a new branch has been created.
@@ -857,6 +861,14 @@ class Repository
end
end
+ def avatar
+ @avatar ||= cache.fetch(:avatar) do
+ AVATAR_FILES.find do |file|
+ blob_at_branch('master', file)
+ end
+ end
+ end
+
private
def cache
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 5f91991f781..d85f7bfdf57 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -5,14 +5,15 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
class Todo < ActiveRecord::Base
@@ -27,7 +28,9 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
- validates :action, :project, :target, :user, presence: true
+ validates :action, :project, :target_type, :user, presence: true
+ validates :target_id, presence: true, unless: :for_commit?
+ validates :commit_id, presence: true, if: :for_commit?
default_scope { reorder(id: :desc) }
@@ -36,7 +39,7 @@ class Todo < ActiveRecord::Base
state_machine :state, initial: :pending do
event :done do
- transition [:pending, :done] => :done
+ transition [:pending] => :done
end
state :pending
@@ -50,4 +53,25 @@ class Todo < ActiveRecord::Base
target.title
end
end
+
+ def for_commit?
+ target_type == "Commit"
+ end
+
+ # override to return commits, which are not active record
+ def target
+ if for_commit?
+ project.commit(commit_id) rescue nil
+ else
+ super
+ end
+ end
+
+ def target_reference
+ if for_commit?
+ target.short_id
+ else
+ target.to_reference
+ end
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 68b242888aa..c011af03591 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -59,6 +59,7 @@
# hide_project_limit :boolean default(FALSE)
# unlock_token :string
# otp_grace_period_started_at :datetime
+# external :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -77,6 +78,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token
default_value_for :admin, false
+ default_value_for :external, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
@@ -171,6 +173,7 @@ class User < ActiveRecord::Base
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
+ before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
after_create :post_create_hook
@@ -218,6 +221,7 @@ class User < ActiveRecord::Base
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
+ scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
@@ -273,6 +277,8 @@ class User < ActiveRecord::Base
self.with_two_factor
when 'wop'
self.without_projects
+ when 'external'
+ self.external
else
self.active
end
@@ -841,4 +847,11 @@ class User < ActiveRecord::Base
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
+
+ def ensure_external_user_rights
+ return unless self.external?
+
+ self.can_create_group = false
+ self.projects_limit = 0
+ end
end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index 9cb918d7a2e..a3c950ede1f 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -9,7 +9,8 @@ module Commits
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
- validate and commit
+ check_push_permissions unless @create_merge_request
+ commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ReversionError => ex
error(ex.message)
@@ -45,11 +46,11 @@ module Commits
end
end
- def validate
+ def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed
- raise_error('You are not allowed to push into this branch')
+ raise ValidationError.new('You are not allowed to push into this branch')
end
true
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index d840ab5e340..14e2a2c0699 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -17,7 +17,7 @@ class GitPushService < BaseService
# 6. Checks if the project's main language has changed
#
def execute
- @project.repository.after_push_commit(branch_name)
+ @project.repository.after_push_commit(branch_name, params[:newrev])
if push_remove_branch?
@project.repository.after_remove_branch
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 954746a39a5..6e9152e444e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -47,6 +47,21 @@ module MergeRequests
merge_request.title = merge_request.source_branch.titleize.humanize
end
+ # When your branch name starts with an iid followed by a dash this pattern will
+ # be interpreted as the use wants to close that issue on this project
+ # Pattern example: 112-fix-mep-mep
+ # Will lead to appending `Closes #112` to the description
+ if match = merge_request.source_branch.match(/-(\d+)\z/)
+ iid = match[1]
+ closes_issue = "Closes ##{iid}"
+
+ if merge_request.description.present?
+ merge_request.description << closes_issue.prepend("\n")
+ else
+ merge_request.description = closes_issue
+ end
+ end
+
merge_request
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 7408e09ed1e..ba50305dbd5 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,11 +1,7 @@
module Projects
class AutocompleteService < BaseService
- def initialize(project)
- @project = project
- end
-
def issues
- @project.issues.opened.select([:iid, :title])
+ @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
def merge_requests
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index bccd67d3dbf..a0973c5d260 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -24,7 +24,7 @@ module Projects
def execute
raise LeaseTaken if !try_obtain_lease
- GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
ensure
@project.update_column(:pushes_since_gc, 0)
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e1e94c5cc38..aa9837038a6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -11,7 +11,7 @@ module Search
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- Gitlab::SearchResults.new(projects, params[:search])
+ Gitlab::SearchResults.new(current_user, projects, params[:search])
end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index c08881dce4b..4b500914cfb 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,8 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project,
+ Gitlab::ProjectSearchResults.new(current_user,
+ project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 58a861ee08e..2afcaad4646 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -207,6 +207,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "Started branch `issue-branch-button-201`"
+ def self.new_issue_branch(issue, project, author, branch)
+ h = Gitlab::Application.routes.url_helpers
+ link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+
+ body = "Started branch [`#{branch}`](#{link})"
+ create_note(noteable: issue, project: project, author: author, note: body)
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 4392e2d17fe..f2662922e90 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -103,24 +103,16 @@ class TodoService
# * mark all pending todos related to the target for the current user as done
#
def mark_pending_todos_as_done(target, user)
- pending_todos(user, target.project, target).update_all(state: :done)
+ attributes = attributes_for_target(target)
+ pending_todos(user, attributes).update_all(state: :done)
end
private
- def create_todos(project, target, author, users, action, note = nil)
+ def create_todos(users, attributes)
Array(users).each do |user|
- next if pending_todos(user, project, target).exists?
-
- Todo.create(
- project: project,
- user_id: user.id,
- author_id: author.id,
- target_id: target.id,
- target_type: target.class.name,
- action: action,
- note: note
- )
+ next if pending_todos(user, attributes).exists?
+ Todo.create(attributes.merge(user_id: user.id))
end
end
@@ -130,8 +122,8 @@ class TodoService
end
def handle_note(note, author)
- # Skip system notes, notes on commit, and notes on project snippet
- return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type)
+ # Skip system notes, and notes on project snippet
+ return if note.system? || note.for_project_snippet?
project = note.project
target = note.noteable
@@ -142,13 +134,39 @@ class TodoService
def create_assignment_todo(issuable, author)
if issuable.assignee && issuable.assignee != author
- create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
+ attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
+ create_todos(issuable.assignee, attributes)
end
end
- def create_mention_todos(project, issuable, author, note = nil)
- mentioned_users = filter_mentioned_users(project, note || issuable, author)
- create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note)
+ def create_mention_todos(project, target, author, note = nil)
+ mentioned_users = filter_mentioned_users(project, note || target, author)
+ attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
+ create_todos(mentioned_users, attributes)
+ end
+
+ def attributes_for_target(target)
+ attributes = {
+ project_id: target.project.id,
+ target_id: target.id,
+ target_type: target.class.name,
+ commit_id: nil
+ }
+
+ if target.is_a?(Commit)
+ attributes.merge!(target_id: nil, commit_id: target.id)
+ end
+
+ attributes
+ end
+
+ def attributes_for_todo(project, target, author, action, note = nil)
+ attributes_for_target(target).merge!(
+ project_id: project.id,
+ author_id: author.id,
+ action: action,
+ note: note
+ )
end
def filter_mentioned_users(project, target, author)
@@ -160,11 +178,8 @@ class TodoService
mentioned_users.uniq
end
- def pending_todos(user, project, target)
- user.todos.pending.where(
- project_id: project.id,
- target_id: target.id,
- target_type: target.class.name
- )
+ def pending_todos(user, criteria = {})
+ valid_keys = [:project_id, :target_id, :target_type, :commit_id]
+ user.todos.pending.where(criteria.slice(*valid_keys))
end
end
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index e18dd9bc905..d2527ede995 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -58,9 +58,15 @@
= f.label :admin, class: 'control-label'
- if current_user == @user
.col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights
+ .col-sm-10 You cannot remove your own admin rights.
- else
.col-sm-10= f.check_box :admin
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10= f.check_box :external
+ .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+
%fieldset
%legend Profile
.form-group
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index b6b1168bd37..0ee8dc962b9 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -19,6 +19,10 @@
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
+ = link_to admin_users_path(filter: 'external') do
+ External
+ %small.badge= number_with_delimiter(User.external.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
@@ -70,12 +74,14 @@
%li
.list-item-name
- if user.blocked?
- %i.fa.fa-lock.cred
+ = icon("lock", class: "cred")
- else
- %i.fa.fa-user.cgreen
+ = icon("user", class: "cgreen")
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
+ - if user.external?
+ %strong.cred (External)
- if user == current_user
%span.cred It's you!
.pull-right
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 2bdbae19588..d37489bebea 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -48,6 +48,10 @@
Disabled
%li
+ %span.light External User:
+ %strong
+ = @user.external? ? "Yes" : "No"
+ %li
%span.light Can create groups:
%strong
= @user.can_create_group ? "Yes" : "No"
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index c3efa7727b1..d54c7cad7be 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-- publicish_project_count = Project.publicish(current_user).count
+- publicish_project_count = ProjectsFinder.new.execute(current_user).count
%h3.page-title Welcome to GitLab!
%p.light Self hosted Git management application.
%hr
@@ -18,7 +18,7 @@
- if current_user.can_create_project?
.link_holder
= link_to new_project_path, class: "btn btn-new" do
- %i.fa.fa-plus
+ = icon('plus')
New Project
- if current_user.can_create_group?
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 45cfe3da188..e3a4d64df01 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -2,7 +2,7 @@
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
- .todo-title
+ .todo-title.title
%span.author-name
- if todo.author
= link_to_author(todo)
@@ -16,7 +16,9 @@
- if todo.pending?
.todo-actions.pull-right
- = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn'
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
.todo-body
.todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 946d7df3933..f9ec3a89158 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -3,13 +3,15 @@
.top-area
%ul.nav-links
- %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')}
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span{class: 'badge'}
= todos_pending_count
- %li{class: ('active' if params[:state] == 'done')}
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -18,7 +20,9 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
.todos-filters
.gray-content-block.second-block
@@ -42,12 +46,12 @@
.prepend-top-default
- if @todos.any?
- @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
+ .panel.panel-default.panel-small.js-todos-list
- project = group[0]
.panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
- %ul.well-list.todos-list
+ %ul.content-list.todos-list
= render group[1]
= paginate @todos, theme: "gitlab"
- else
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 36fb2d51629..2d9d9dd6342 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,4 +1,4 @@
-- if event.proper?
+- if event.proper?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 77d01a7736c..f3090b96702 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -46,6 +46,8 @@
%h1.title= title
= render 'shared/outdated_browser'
+
- if @project && !@project.empty_repo?
- :javascript
- var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}";
+ - if ref = @ref || @project.repository.root_ref
+ :javascript
+ var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}";
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 0ae83ee01eb..86b46e8c75e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count)
+ %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
new file mode 100644
index 00000000000..95ab9ecf3e8
--- /dev/null
+++ b/app/views/projects/_builds_settings.html.haml
@@ -0,0 +1,60 @@
+%fieldset.builds-feature
+ %legend
+ Builds:
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = f.label :build_allow_git_fetch_false do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr Slower but makes sure you have a clean dir before every build
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr Faster
+
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block per build in minutes
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ %p.help-block
+ We will use this regular expression to find test coverage output in build trace.
+ Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public builds
+ .help-block Allow everyone to access builds for Public and Internal projects
+
+ .form-group
+ = f.label :runners_token, "Runners token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used to checkout project.
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 3ac058a3bf8..dc34032b1b8 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -42,13 +42,17 @@
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- - if blob_text_viewable?(blob)
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, index: i
- - elsif blob.image?
- - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
+ - if diff_file.too_large?
+ .nothing-here-block
+ This diff could not be displayed because it is too large.
- else
- .nothing-here-block No preview for this file type
+ - if blob_text_viewable?(blob)
+ - if diff_view == 'parallel'
+ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
+ - else
+ = render "projects/diffs/text_file", diff_file: diff_file, index: i
+ - elsif blob.image?
+ - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
+ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
+ - else
+ .nothing-here-block No preview for this file type
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index f2e56081afe..6d872cd0b21 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -84,6 +84,8 @@
%br
%span.descr Share code pastes with others out of git repository
+ = render 'builds_settings', f: f
+
%fieldset.features
%legend
Project avatar:
@@ -110,69 +112,6 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- %fieldset.features
- %legend
- Continuous Integration
- .form-group
- .col-sm-offset-2.col-sm-10
- %p Get recent application code using the following command:
- .radio
- = f.label :build_allow_git_fetch_false do
- = f.radio_button :build_allow_git_fetch, 'false'
- %strong git clone
- %br
- %span.descr Slower but makes sure you have a clean dir before every build
- .radio
- = f.label :build_allow_git_fetch_true do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr Faster
-
- .form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
- .col-sm-10
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block per build in minutes
- .form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
- .col-sm-10
- .input-group
- %span.input-group-addon /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- %p.help-block
- We will use this regular expression to find test coverage output in build trace.
- Leave blank if you want to disable this feature
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %ul
- %li
- Simplecov (Ruby) -
- %code \(\d+.\d+\%\) covered
- %li
- pytest-cov (Python) -
- %code \d+\%\s*$
- %li
- phpunit --coverage-text --colors=never (PHP) -
- %code ^\s*Lines:\s*\d+.\d+\%
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :public_builds do
- = f.check_box :public_builds
- %strong Public builds
- .help-block Allow everyone to access builds for Public and Internal projects
-
- %fieldset.features
- %legend
- Advanced settings
- .form-group
- = f.label :runners_token, "CI token", class: 'control-label'
- .col-sm-10
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used to checkout project.
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index a44f34c2a68..4aa92d0b39e 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -3,10 +3,11 @@
.issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
- .issue-title
+ .issue-title.title
%span.issue-title-text
- = link_to_gfm issue.title, issue_path(issue), class: "title"
- %ul.controls.light
+ = confidential_icon(issue)
+ = link_to_gfm issue.title, issue_path(issue)
+ %ul.controls
- if issue.closed?
%li
CLOSED
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index d9868ad1f0a..d6b38b327ff 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,4 +1,4 @@
--if @merge_requests.any?
+- if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
new file mode 100644
index 00000000000..e66e4669d48
--- /dev/null
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -0,0 +1,5 @@
+- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ .pull-right
+ = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do
+ = icon('code-fork')
+ New Branch
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
new file mode 100644
index 00000000000..b10cd03515f
--- /dev/null
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -0,0 +1,15 @@
+- if @related_branches.any?
+ %h2.related-branches-title
+ = pluralize(@related_branches.count, 'Related Branch')
+ %ul.unstyled-list
+ - @related_branches.each do |branch|
+ %li
+ - sha = @project.repository.find_branch(branch).target
+ - ci_commit = @project.ci_commit(sha) if sha
+ - if ci_commit
+ %span.related-branch-ci-status
+ = render_ci_status(ci_commit)
+ %span.related-branch-info
+ %strong
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
+ = branch
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 0242276cd84..52df3de8a27 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -22,20 +22,20 @@
= icon('angle-double-left')
.issue-meta
+ = confidential_icon(@issue)
%strong.identifier
Issue ##{@issue.iid}
%span.creator
- by
+ opened
.editor-details
.editor-details
+ = time_ago_with_tooltip(@issue.created_at)
+ by
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
- %span.hidden-xs
- = '@' + @issue.author.username
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
- = time_ago_with_tooltip(@issue.created_at)
.pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project)
@@ -63,15 +63,14 @@
= markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
- - if @issue.updated_at != @issue.created_at
- %small
- Edited
- = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
+ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
.merge-requests
= render 'merge_requests'
+ = render 'related_branches'
.content-block.content-block-small
+ = render 'new_branch'
= render 'votes/votes_block', votable: @issue
.row
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 18cf3f14f0b..13d0cbdde1d 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,8 +1,8 @@
%li{ class: mr_css_classes(merge_request) }
- .merge-request-title
+ .merge-request-title.title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title"
- %ul.controls.light
+ = link_to_gfm merge_request.title, merge_request_path(merge_request)
+ %ul.controls
- if merge_request.merged?
%li
MERGED
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index 602f787e6cf..a23bd8d18d0 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -11,7 +11,4 @@
%textarea.hidden.js-task-list-field
= @merge_request.description
- - if @merge_request.updated_at != @merge_request.created_at
- %small
- Edited
- = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom')
+ = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index a75c0d96c57..eeb605e2dc5 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -8,18 +8,21 @@
= icon('angle-double-left')
.issue-meta
%strong.identifier
- Merge Request ##{@merge_request.iid}
+ %span.hidden-sm.hidden-md.hidden-lg
+ MR
+ %span.hidden-xs
+ Merge Request
+ !#{@merge_request.iid}
%span.creator
- by
+ opened
.editor-details
+ = time_ago_with_tooltip(@merge_request.created_at)
+ by
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
- %span.hidden-xs
- = '@' + @merge_request.author.username
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
- = time_ago_with_tooltip(@merge_request.created_at)
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b4597043a27..be63875ab34 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -42,7 +42,7 @@
= preserve do
= markdown @milestone.description
-- if @milestone.complete? && @milestone.active?
+- if @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 52972576aff..2cf32e6093d 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -27,20 +27,13 @@
%span.note-last-update
%a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
- - if note.updated_at != note.created_at
- %span.note-updated-at
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
- - if note.updated_by && note.updated_by != note.author
- by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
-
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note)
= render 'projects/notes/edit_form', note: note
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note.attachment.url
.note-attachment
@@ -54,4 +47,3 @@
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')
- .clear
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index b9486a9b492..24658319060 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -10,7 +10,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 45d700781f3..710f5613c81 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,5 +1,6 @@
.search-result-row
%h4
+ = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
.pull-right ##{issue.iid}
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index fb9a8db0889..f172350f5ff 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -10,7 +10,7 @@
%i.fa.fa-cogs
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
- %i.fa.fa-sign-out
+ = icon('sign-out')
.stats
%span
@@ -22,12 +22,13 @@
= number_with_delimiter(group.users.count)
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
- = link_to group, class: 'group-name title' do
- = group.name
+ .title
+ = link_to group, class: 'group-name' do
+ = group.name
- - if group_member
- as
- %span #{group_member.human_access}
+ - if group_member
+ as
+ %span #{group_member.human_access}
- if group.description.present?
.description
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 3eb0db276b2..ac20f7d1f7e 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -9,75 +9,20 @@
.filter-item.inline
- if params[:author_id]
= hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } })
+ = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id])
- = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
+ = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
- - if params[:milestone_title]
- = hidden_field_tag(:milestone_title, params[:milestone_title])
- = dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
- placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
- - if @project
- %ul.dropdown-footer-list
- - if can? current_user, :admin_milestone, @project
- %li
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
- Create new
- %li
- = link_to namespace_project_milestones_path(@project.namespace, @project) do
- - if can? current_user, :admin_milestone, @project
- Manage milestones
- - else
- View milestones
+ = render "shared/issuable/milestone_dropdown"
.filter-item.inline.labels-filter
- - if params[:label_name]
- = hidden_field_tag(:label_name, params[:label_name])
- .dropdown
- %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
- %span.dropdown-toggle-text
- Label
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- .dropdown-page-one
- = dropdown_title("Filter by label")
- = dropdown_filter("Search labels")
- = dropdown_content
- - if @project
- = dropdown_footer do
- %ul.dropdown-footer-list
- - if can? current_user, :admin_label, @project
- %li
- %a.dropdown-toggle-page{href: "#"}
- Create new
- %li
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- - if can? current_user, :admin_label, @project
- Manage labels
- - else
- View labels
- - if can? current_user, :admin_label, @project
- .dropdown-page-two
- = dropdown_title("Create new label", back: true)
- = dropdown_content do
- %input#new_label_color{type: "hidden"}
- %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
- .dropdown-label-color-preview.js-dropdown-label-color-preview
- .suggest-colors.suggest-colors-dropdown
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp
- %button.btn.btn-primary.js-new-label-btn{type: "button"}
- Create
- = dropdown_loading
- .dropdown-loading
- = icon('spinner spin')
+ = render "shared/issuable/label_dropdown"
.pull-right
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index d5a4aad05d9..9ef729e960c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -29,6 +29,15 @@
= render 'projects/notes/hints'
.clearfix
.error-alert
+
+- if issuable.is_a?(Issue) && !issuable.project.private?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :confidential do
+ = f.check_box :confidential
+ This issue is confidential and should only be visible to team members
+
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
%hr
.form-group
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
new file mode 100644
index 00000000000..87617315181
--- /dev/null
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -0,0 +1,39 @@
+- if params[:label_name]
+ = hidden_field_tag(:label_name, params[:label_name])
+.dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
+ %span.dropdown-toggle-text
+ = h(params[:label_name].presence || "Label")
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Filter by label")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ - if can? current_user, :admin_label, @project and @project
+ .dropdown-page-two
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ %input#new_label_color{type: "hidden"}
+ %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
+ .dropdown-label-color-preview.js-dropdown-label-color-preview
+ .suggest-colors.suggest-colors-dropdown
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp
+ %button.btn.btn-primary.js-new-label-btn{type: "button"}
+ Create
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
new file mode 100644
index 00000000000..0434506c8d7
--- /dev/null
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -0,0 +1,16 @@
+- if params[:milestone_title]
+ = hidden_field_tag(:milestone_title, params[:milestone_title])
+= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ - if @project
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_milestone, @project
+ %li
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ Create new
+ %li
+ = link_to namespace_project_milestones_path(@project.namespace, @project) do
+ - if can? current_user, :admin_milestone, @project
+ Manage milestones
+ - else
+ View milestones
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index f1d92ef48b2..3fb409ff727 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -1,3 +1,6 @@
+- participants_row = 7
+- participants_size = participants.size
+- participants_extra = participants_size - participants_row
.block.participants
.sidebar-collapsed-icon
= icon('users')
@@ -5,6 +8,13 @@
= participants.count
.title.hide-collapsed
= pluralize participants.count, "participant"
- - participants.each do |participant|
- %span.hide-collapsed
- = link_to_member(@project, participant, name: false, size: 24)
+ .hide-collapsed.participants-list
+ - participants.each do |participant|
+ .participants-author.js-participants-author
+ = link_to_member(@project, participant, name: false, size: 24)
+ - if participants_extra > 0
+ %div.participants-more
+ %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ + #{participants_extra} more
+:javascript
+ Issue.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 23b1ed1e51b..2b95b19facc 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,13 +1,12 @@
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- .block
+ .block.issuable-sidebar-header
%span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
- %span.pull-right
- %a.gutter-toggle.js-sidebar-toggle{href: '#'}
- = sidebar_gutter_toggle_icon
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+ = sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
@@ -22,20 +21,20 @@
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)}
- if issuable.assignee
- = link_to_member_avatar(issuable.assignee, size: 24)
+ = link_to_member(@project, issuable.assignee, size: 24)
- else
= icon('user')
.title.hide-collapsed
- %label
- Assignee
+ Assignee
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
+ = link_to_member(@project, issuable.assignee, size: 32) do
+ %span.username
+ = issuable.assignee.to_reference
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
= icon('exclamation-triangle')
@@ -54,18 +53,13 @@
- else
No
.title.hide-collapsed
- %label
- Milestone
+ Milestone
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.milestone
- %span.back-to-milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %strong
- = icon('clock-o')
- = issuable.milestone.title
+ = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
+ = issuable.milestone.title
- else
.light None
.selectbox.hide-collapsed
@@ -80,11 +74,10 @@
%span
= issuable.labels.count
.title.hide-collapsed
- %label Labels
+ Labels
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)}
- if issuable.labels.any?
- issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
@@ -95,14 +88,13 @@
{ selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- %hr
- if current_user
- subscribed = issuable.subscribed?(current_user)
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
.title.hide-collapsed
- %label.light Notifications
+ Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index f7c6fc14adf..85888096722 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -10,6 +10,8 @@
%strong #{project.name} &middot;
- elsif show_full_project_name
%strong #{project.name_with_namespace} &middot;
+ - if issuable.is_a?(Issue)
+ = confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
%div{class: 'issuable-detail'}
= link_to [project.namespace.becomes(Namespace), project, issuable] do
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index f01138af3f0..6b25745c554 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -6,10 +6,10 @@
.col-sm-6
%strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
.col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
+ .pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
- = link_to pluralize(milestone.issues.size, 'Issue'), issues_path
+ = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 59d4ae29f79..385c6596606 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -3,15 +3,15 @@
.context.prepend-top-default
.milestone-summary
%h4 Progress
- %strong= milestone.issues.size
+ %strong= milestone.issues_visible_to_user(current_user).size
issues:
%span.milestone-stat
- %strong= milestone.issues.opened.size
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
open and
- %strong= milestone.issues.closed.size
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%span.milestone-stat
- %strong== #{milestone.percent_complete}%
+ %strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 57d7ee85a3b..2b6ce2d7e7a 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -2,7 +2,7 @@
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
- %span.badge= milestone.issues.size
+ %span.badge= milestone.issues_visible_to_user(current_user).size
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
Merge Requests
@@ -21,7 +21,7 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues
- = render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 4cf1d948b5b..cab8743a077 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -28,7 +28,7 @@
%h2.title
= markdown escape_once(milestone.title), pipeline: :single_line
-- if milestone.complete? && milestone.active?
+- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
@@ -47,7 +47,7 @@
- project_name = group ? ms.project.name : ms.project.name_with_namespace
= link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms)
%td
- = ms.issues.opened.count
+ = ms.issues_visible_to_user(current_user).opened.count
%td
- if ms.closed?
Closed
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 97cfb76cdb0..872d2bdf46d 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -7,26 +7,11 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit
-- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2']
+- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
- cache_key.push(ci_commit.status) if ci_commit
%li.project-row{ class: css_class }
= cache(cache_key) do
- = link_to project_path(project), class: dom_class(project) do
- - if avatar
- .dash-project-avatar
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name.title
- %span.namespace-name
- - if project.namespace && !skip_namespace
- = project.namespace.human_name
- \/
- %span.project-name.filter-title
- = project.name
-
.controls
- if project.main_language
%span
@@ -45,6 +30,23 @@
%span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' },
title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"}
= visibility_level_icon(project.visibility_level, fw: false)
+
+ .title
+ = link_to project_path(project), class: dom_class(project) do
+ - if avatar
+ .dash-project-avatar
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace && !skip_namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name.filter-title
+ = project.name
+
- if show_last_commit_as_description
.description
= link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index a316a085107..c96dfefe17f 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,8 +1,8 @@
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
- .snippet-title
- = link_to reliable_snippet_path(snippet), class: 'title' do
+ .title
+ = link_to reliable_snippet_path(snippet) do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
diff --git a/app/workers/gitlab_shell_one_shot_worker.rb b/app/workers/gitlab_shell_one_shot_worker.rb
new file mode 100644
index 00000000000..4ddbcf574d5
--- /dev/null
+++ b/app/workers/gitlab_shell_one_shot_worker.rb
@@ -0,0 +1,10 @@
+class GitlabShellOneShotWorker
+ include Sidekiq::Worker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options queue: :gitlab_shell, retry: false
+
+ def perform(action, *arg)
+ gitlab_shell.send(action, *arg)
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 14d7813412e..3cc232ef1ae 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,6 +1,5 @@
class PostReceive
include Sidekiq::Worker
- include Gitlab::Identifier
sidekiq_options queue: :post_receive
@@ -11,51 +10,44 @@ class PostReceive
log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"")
end
- repo_path.gsub!(/\.git\z/, "")
- repo_path.gsub!(/\A\//, "")
+ post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
- project = Project.find_with_namespace(repo_path)
-
- if project.nil?
+ if post_received.project.nil?
log("Triggered hook for non-existing project with full path \"#{repo_path} \"")
return false
end
- changes = Base64.decode64(changes) unless changes.include?(" ")
- changes = utf8_encode_changes(changes)
- changes = changes.lines
+ if post_received.wiki?
+ # Nothing defined here yet.
+ elsif post_received.regular_project?
+ process_project_changes(post_received)
+ else
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"")
+ false
+ end
+ end
- changes.each do |change|
+ def process_project_changes(post_received)
+ post_received.changes.each do |change|
oldrev, newrev, ref = change.strip.split(' ')
- @user ||= identify(identifier, project, newrev)
+ @user ||= post_received.identify(newrev)
unless @user
- log("Triggered hook for non-existing user \"#{identifier} \"")
+ log("Triggered hook for non-existing user \"#{post_received.identifier} \"")
return false
end
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new.execute(project, @user, oldrev, newrev, ref)
+ GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
else
- GitPushService.new(project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
+ GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
end
end
- def utf8_encode_changes(changes)
- changes = changes.dup
-
- changes.force_encoding("UTF-8")
- return changes if changes.valid_encoding?
-
- # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON.
- detection = CharlockHolmes::EncodingDetector.detect(changes)
- return changes unless detection && detection[:encoding]
-
- CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
- end
-
+ private
+
def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
diff --git a/config/routes.rb b/config/routes.rb
index 2ae282f48a6..ec79522002e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -351,6 +351,8 @@ Rails.application.routes.draw do
get :issues
get :merge_requests
get :activity
+ get :labels
+ get :milestones
scope module: :dashboard do
resources :milestones, only: [:index, :show]
diff --git a/db/migrate/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb
new file mode 100644
index 00000000000..e9d47fd589a
--- /dev/null
+++ b/db/migrate/20160223192159_add_confidential_to_issues.rb
@@ -0,0 +1,6 @@
+class AddConfidentialToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :confidential, :boolean, default: false
+ add_index :issues, :confidential
+ end
+end
diff --git a/db/migrate/20160310185910_add_external_flag_to_users.rb b/db/migrate/20160310185910_add_external_flag_to_users.rb
new file mode 100644
index 00000000000..54937f1eb71
--- /dev/null
+++ b/db/migrate/20160310185910_add_external_flag_to_users.rb
@@ -0,0 +1,5 @@
+class AddExternalFlagToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :external, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
new file mode 100644
index 00000000000..6871b3920df
--- /dev/null
+++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
@@ -0,0 +1,5 @@
+class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration
+ def change
+ change_column_null :todos, :target_id, true
+ end
+end
diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb
new file mode 100644
index 00000000000..ae19fdd1abd
--- /dev/null
+++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb
@@ -0,0 +1,6 @@
+class AddCommitIdToTodos < ActiveRecord::Migration
+ def change
+ add_column :todos, :commit_id, :string
+ add_index :todos, :commit_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2c27b228864..5b2f5aa3ddd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160316123110) do
+ActiveRecord::Schema.define(version: 20160316204731) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -416,10 +416,12 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
+ t.boolean "confidential", default: false
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
+ add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -865,7 +867,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
create_table "todos", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
- t.integer "target_id", null: false
+ t.integer "target_id"
t.string "target_type", null: false
t.integer "author_id"
t.integer "action", null: false
@@ -873,9 +875,11 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "note_id"
+ t.string "commit_id"
end
add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
+ add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
add_index "todos", ["state"], name: "index_todos_on_state", using: :btree
@@ -940,6 +944,7 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
+ t.boolean "external", default: false
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 0ca30e4e0f2..08d0a6a5bfb 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,12 +3,13 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
+- [CI](ci/README.md)
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
-- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do.
+- [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
@@ -16,42 +17,6 @@
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-## CI User documentation
-
-- [Get started with GitLab CI](ci/quick_start/README.md)
-- [Learn how to enable or disable GitLab CI](ci/enable_or_disable_ci.md)
-- [Learn how `.gitlab-ci.yml` works](ci/yaml/README.md)
-- [Configure a Runner, the application that runs your builds](ci/runners/README.md)
-- [Use Docker images with GitLab Runner](ci/docker/using_docker_images.md)
-- [Use CI to build Docker images](ci/docker/using_docker_build.md)
-- [Use variables in your `.gitlab-ci.yml`](ci/variables/README.md)
-- [Use SSH keys in your build environment](ci/ssh_keys/README.md)
-- [Trigger builds through the API](ci/triggers/README.md)
-- [Build artifacts](ci/build_artifacts/README.md)
-- [User permissions](ci/permissions/README.md)
-- [API](ci/api/README.md)
-
-### CI Examples
-
-- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [Test your PHP applications](ci/examples/php.md)
-- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
-- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
-- [Test Clojure applications](ci/examples/test-clojure-application.md)
-- [Using `dpl` as deployment tool](ci/deployment/README.md)
-- Help your favorite programming language and GitLab by sending a merge request
- with a guide for that language.
-
-### CI Services
-
-GitLab CI uses the `services` keyword to define what docker containers should
-be linked with your base image. Below is a list of examples you may use:
-
-- [Using MySQL](ci/services/mysql.md)
-- [Using PostgreSQL](ci/services/postgres.md)
-- [Using Redis](ci/services/redis.md)
-- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
-
## Administrator documentation
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
diff --git a/doc/api/users.md b/doc/api/users.md
index 82c57a2fd43..383e7c76ab0 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -194,6 +194,7 @@ Parameters:
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false
+- `external` (optional) - Flags the user as external - true or false(default)
## User modification
@@ -219,6 +220,7 @@ Parameters:
- `bio` - User's biography
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
+- `external` (optional) - Flags the user as external - true or false(default)
Note, at the moment this method does only return a 404 error,
even in cases where a 409 (Conflict) would be more appropriate,
@@ -560,7 +562,7 @@ Parameters:
- `uid` (required) - id of specified user
-Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
## Unblock user
diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md
index ffd841ca9e7..7d91ce6710f 100644
--- a/doc/ci/deployment/README.md
+++ b/doc/ci/deployment/README.md
@@ -89,7 +89,7 @@ We also use two secure variables:
In GitLab CI 7.12 a new feature was introduced: Secure Variables.
Secure Variables can added by going to `Project > Variables > Add Variable`.
**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
-The variables that are defined in the project settings are send along with the build script to the runner.
+The variables that are defined in the project settings are sent along with the build script to the runner.
The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
It is also important that secret's value is hidden in the build log.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5158e3c387c..762b35859b9 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -135,6 +135,9 @@ thus allowing to fine tune them.
### cache
+>**Note:**
+Introduced in GitLab Runner v0.7.0.
+
`cache` is used to specify a list of files and directories which should be
cached between builds.
@@ -143,15 +146,55 @@ cached between builds.
If `cache` is defined outside the scope of the jobs, it means it is set
globally and all jobs will use its definition.
-To cache all git untracked files and files in `binaries`:
+Cache all files in `binaries` and `.config`:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+ - .config
+```
+
+Cache all Git untracked files:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ untracked: true
+```
+
+Cache all Git untracked files and files in `binaries`:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ untracked: true
+ paths:
+ - binaries/
+```
+
+Locally defined cache overwrites globally defined options. This will cache only
+`binaries/`:
```yaml
cache:
- untracked: true
paths:
- - binaries/
+ - my/files
+
+rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
```
+The cache is provided on best effort basis, so don't expect that cache will be
+always present. For implementation details please check GitLab Runner.
+
#### cache:key
>**Note:**
@@ -236,6 +279,8 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
+| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
+| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
@@ -418,14 +463,14 @@ artifacts:
- .config
```
-Send all git untracked files:
+Send all Git untracked files:
```yaml
artifacts:
untracked: true
```
-Send all git untracked files and files in `binaries`:
+Send all Git untracked files and files in `binaries`:
```yaml
artifacts:
@@ -579,63 +624,6 @@ deploy:
script: make deploy
```
-### cache
-
->**Note:**
-Introduced in GitLab Runner v0.7.0.
-
-`cache` is used to specify list of files and directories which should be cached
-between builds. Below are some examples:
-
-Cache all files in `binaries` and `.config`:
-
-```yaml
-rspec:
- script: test
- cache:
- paths:
- - binaries/
- - .config
-```
-
-Cache all git untracked files:
-
-```yaml
-rspec:
- script: test
- cache:
- untracked: true
-```
-
-Cache all git untracked files and files in `binaries`:
-
-```yaml
-rspec:
- script: test
- cache:
- untracked: true
- paths:
- - binaries/
-```
-
-Locally defined cache overwrites globally defined options. This will cache only
-`binaries/`:
-
-```yaml
-cache:
- paths:
- - my/files
-
-rspec:
- script: test
- cache:
- paths:
- - binaries/
-```
-
-The cache is provided on best effort basis, so don't expect that cache will be
-always present. For implementation details please check GitLab Runner.
-
## Hidden jobs
>**Note:**
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index 15051dd76f9..dcdf49d3379 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore [webhooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
+Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index aa989417c4b..c567846f624 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -76,7 +76,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 2.7.3 or higher
+ # Make sure Git is version 2.7.4 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -89,9 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.3.tar.gz
- echo '30d067499b61caddedaf1a407b4947244f14d10842d100f7c7c6ea1c288280cd git-2.7.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.3.tar.gz
- cd git-2.7.3/
+ curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
+ echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz
+ cd git-2.7.4/
./configure
make prefix=/usr/local all
@@ -161,7 +161,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Install the database packages
sudo apt-get install -y postgresql postgresql-client libpq-dev
-
+
# Create a user for GitLab
sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;"
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index ac0fd3d1756..3d375e47c8e 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -71,3 +71,24 @@ Any user can remove themselves from a group, unless they are the last Owner of t
| Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
+
+## External Users
+
+In cases where it is desired that a user has access only to some internal or
+private projects, there is the option of creating **External Users**. This
+feature may be useful when for example a contractor is working on a given
+project and should only have access to that project.
+
+External users can only access projects to which they are explicitly granted
+access, thus hiding all other internal or private ones from them. Access can be
+granted by adding the user as member to the project or group.
+
+They will, like usual users, receive a role in the project or group with all
+the abilities that are mentioned in the table above. They cannot however create
+groups or projects, and they have the same access as logged out users in all
+other cases.
+
+An administrator can flag a user as external [through the API](../api/users.md)
+or by checking the checkbox on the admin panel. As an administrator, navigate
+to **Admin > Users** to create a new user or edit an existing one. There, you
+will find the option to flag the user as external.
diff --git a/doc/release/security.md b/doc/release/security.md
index b1a62b333e6..118c016ba4f 100644
--- a/doc/release/security.md
+++ b/doc/release/security.md
@@ -15,7 +15,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
1. Verify that the issue can be reproduced
1. Acknowledge the issue to the researcher that disclosed it
1. Inform the release manager that there needs to be a security release
-1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server"
+1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server"
1. The MR with the security fix should get a 'security' label and be assigned to the release manager
1. Build the package for GitLab.com and do a deploy
1. Build the package for ci.gitLab.com and do a deploy
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index 8365bdb7b1b..c8499380c18 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
-[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
+[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md)
## Enabling 2FA
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 2ca4e1f3770..9f5c6c4dc84 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -1,5 +1,14 @@
# From 8.2 to 8.3
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
**NOTE:** GitLab 8.0 introduced several significant changes related to
installation and configuration which *are not duplicated here*. Be sure you're
already running a working version of at least 8.0 before proceeding with this
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 269deec7a9c..9f6517d9487 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -1,5 +1,14 @@
# From 8.3 to 8.4
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0a9cb5683e7..0cb137a03cc 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -1,5 +1,14 @@
# From 8.4 to 8.5
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 024f6e8a433..7d63915af5e 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -1,5 +1,14 @@
# From 8.5 to 8.6
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 1e9825e2e10..520c4216295 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,6 +1,6 @@
# Import your project from Bitbucket to GitLab
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md).
+It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
* Sign in to GitLab.com and go to your dashboard
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index fdf9a8d391c..d854ec1e025 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -12,7 +12,7 @@ A protected branch does three simple things:
You can make any branch a protected branch. GitLab makes the master branch a protected branch by default.
-To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md).
+To protect a branch, user needs to have at least a Master permission level, see [permissions document](../permissions/permissions.md).
![protected branches page](protected_branches/protected_branches1.png)
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
index 2945bb3753a..f0fd414a9f9 100644
--- a/features/project/issues/award_emoji.feature
+++ b/features/project/issues/award_emoji.feature
@@ -18,21 +18,24 @@ Feature: Award Emoji
@javascript
Scenario: I add and remove custom award in the issue
Given I click to emoji-picker
- Then The search field is focused
- And I click to emoji in the picker
+ Then The emoji menu is visible
+ And The search field is focused
+ Then I click to emoji in the picker
Then I have award added
And I can remove it by clicking to icon
@javascript
Scenario: I can see the list of emoji categories
Given I click to emoji-picker
- Then The search field is focused
+ Then The emoji menu is visible
+ And The search field is focused
Then I can see the activity and food categories
@javascript
Scenario: I can search emoji
Given I click to emoji-picker
- Then The search field is focused
+ Then The emoji menu is visible
+ And The search field is focused
And I search "hand"
Then I see search result for "hand"
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 74685d24a7d..823658b4f24 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -325,3 +325,11 @@ Feature: Project Merge Requests
When I click the "Target branch" dropdown
And I select a new target branch
Then I should see new target branch changes
+
+ @javascript
+ Scenario: I can close merge request after commenting
+ Given I visit merge request page "Bug NS-04"
+ And I leave a comment like "XML attached"
+ Then I should see comment "XML attached"
+ And I click link "Close"
+ Then I should see closed merge request "Bug NS-04"
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 9722a5a848c..963e4f21365 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -41,7 +41,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
- expect(page).to have_content 'Todo was successfully marked as done.'
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 7a6ae15ffa5..e5b7db4c5e3 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -35,7 +35,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'I should see projects activity feed' do
- expect(page).to have_content 'closed issue'
+ expect(page).to have_content 'joined project'
end
step 'I should see issues from group "Owned" assigned to me' do
diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb
index 47540f356e9..66a48a176e5 100644
--- a/features/steps/project/badges/build.rb
+++ b/features/steps/project/badges/build.rb
@@ -21,7 +21,7 @@ class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps
end
step 'I should see a badge that has not been cached' do
- expect(page.response_headers).to include('Cache-Control' => 'no-cache')
+ expect(page.response_headers['Cache-Control']).to include 'no-cache'
end
def expect_badge(status)
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index ce2554bc80d..c5d45709b44 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -92,6 +92,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
end
+ step 'The emoji menu is visible' do
+ page.find(".emoji-menu.is-visible")
+ end
+
step 'The search field is focused' do
expect(page).to have_selector('#emoji_search')
expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search')
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9805e53624e..71197205f34 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -31,6 +31,7 @@ module API
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
expose :two_factor_enabled
+ expose :external
end
class UserLogin < UserFull
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 252744515da..fda6f841438 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -82,7 +82,7 @@ module API
# GET /projects/:id/issues?milestone=1.0.0&state=closed
# GET /issues?iid=42
get ":id/issues" do
- issues = user_project.issues
+ issues = user_project.issues.visible_to_user(current_user)
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
@@ -104,6 +104,7 @@ module API
# GET /projects/:id/issues/:issue_id
get ":id/issues/:issue_id" do
@issue = user_project.issues.find(params[:issue_id])
+ not_found! unless can?(current_user, :read_issue, @issue)
present @issue, with: Entities::Issue
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index fd2128bd179..13ab17c6904 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -61,19 +61,20 @@ module API
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false
+ # external - Flags the user as external - true or false(default)
# Example Request:
# POST /users
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs)
user.admin = admin unless admin.nil?
user.skip_confirmation! unless confirm
-
identity_attrs = attributes_for_keys [:provider, :extern_uid]
+
if identity_attrs.any?
user.identities.build(identity_attrs)
end
@@ -107,12 +108,13 @@ module API
# bio - Bio
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
+ # external - Flags the user as external - true or false(default)
# Example Request:
# PUT /users/:id
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external]
user = User.find(params[:id])
not_found!('User') unless user
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 9f08aa36e8b..2732e0b5145 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -9,6 +9,11 @@ module Banzai
Issue
end
+ def self.user_can_see_reference?(user, node, context)
+ issue = Issue.find(node.attr('data-issue')) rescue nil
+ Ability.abilities.allowed?(user, :read_issue, issue)
+ end
+
def find_object(project, id)
project.get_issue(id)
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index faa2830c16e..d2e85cabf72 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -24,6 +24,10 @@ module Gitlab
@lines ||= parser.parse(raw_diff.each_line).to_a
end
+ def too_large?
+ diff.too_large?
+ end
+
def highlighted_diff_lines
Gitlab::Diff::Highlight.new(self).highlight
end
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
new file mode 100644
index 00000000000..a088e19d1e7
--- /dev/null
+++ b/lib/gitlab/git_post_receive.rb
@@ -0,0 +1,60 @@
+module Gitlab
+ class GitPostReceive
+ include Gitlab::Identifier
+ attr_reader :repo_path, :identifier, :changes, :project
+
+ def initialize(repo_path, identifier, changes)
+ repo_path.gsub!(/\.git\z/, '')
+ repo_path.gsub!(/\A\//, '')
+
+ @repo_path = repo_path
+ @identifier = identifier
+ @changes = deserialize_changes(changes)
+
+ retrieve_project_and_type
+ end
+
+ def wiki?
+ @type == :wiki
+ end
+
+ def regular_project?
+ @type == :project
+ end
+
+ def identify(revision)
+ super(identifier, project, revision)
+ end
+
+ private
+
+ def retrieve_project_and_type
+ @type = :project
+ @project = Project.find_with_namespace(@repo_path)
+
+ if @repo_path.end_with?('.wiki') && !@project
+ @type = :wiki
+ @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, ''))
+ end
+ end
+
+ def deserialize_changes(changes)
+ changes = Base64.decode64(changes) unless changes.include?(' ')
+ changes = utf8_encode_changes(changes)
+ changes.lines
+ end
+
+ def utf8_encode_changes(changes)
+ changes = changes.dup
+
+ changes.force_encoding('UTF-8')
+ return changes if changes.valid_encoding?
+
+ # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON.
+ detection = CharlockHolmes::EncodingDetector.detect(changes)
+ return changes unless detection && detection[:encoding]
+
+ CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0607a8b9592..71c5b6801fb 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,7 +2,8 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
- def initialize(project, query, repository_ref = nil)
+ def initialize(current_user, project, query, repository_ref = nil)
+ @current_user = current_user
@project = project
@repository_ref = if repository_ref.present?
repository_ref
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index f13528a2eea..f8ab2b1f09e 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,12 +1,13 @@
module Gitlab
class SearchResults
- attr_reader :query
+ attr_reader :current_user, :query
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_projects
- def initialize(limit_projects, query)
+ def initialize(current_user, limit_projects, query)
+ @current_user = current_user
@limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present?
end
@@ -58,7 +59,7 @@ module Gitlab
end
def issues
- issues = Issue.where(project_id: project_ids_relation)
+ issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index fc5475c4eef..1324e4cd267 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -30,7 +30,6 @@ server {
listen [::]:80 default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
- root /home/git/gitlab/public;
## See app/controllers/application_controller.rb for headers set
@@ -57,4 +56,14 @@ server {
proxy_pass http://gitlab-workhorse;
}
+
+ error_page 404 /404.html;
+ error_page 422 /422.html;
+ error_page 500 /500.html;
+ error_page 502 /502.html;
+ location ~ ^/(404|422|500|502)\.html$ {
+ root /home/git/gitlab/public;
+ internal;
+ }
+
}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 1e5f85413ec..af6ea9ed706 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -45,7 +45,6 @@ server {
listen [::]:443 ipv6only=on ssl default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
- root /home/git/gitlab/public;
## Strong SSL Security
## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
@@ -101,4 +100,13 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://gitlab-workhorse;
}
+
+ error_page 404 /404.html;
+ error_page 422 /422.html;
+ error_page 500 /500.html;
+ error_page 502 /502.html;
+ location ~ ^/(404|422|500|502)\.html$ {
+ root /home/git/gitlab/public;
+ internal;
+ }
}
diff --git a/public/404.html b/public/404.html
index a0106bc760d..4862770cc2a 100644
--- a/public/404.html
+++ b/public/404.html
@@ -2,11 +2,51 @@
<html>
<head>
<title>The page you're looking for could not be found (404)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <h1>404</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 404
+ </h1>
<h3>The page you're looking for could not be found.</h3>
<hr/>
<p>Make sure the address is correct and that the page hasn't moved.</p>
diff --git a/public/422.html b/public/422.html
index 026997b48e3..055b0bde165 100644
--- a/public/422.html
+++ b/public/422.html
@@ -2,12 +2,51 @@
<html>
<head>
<title>The change you requested was rejected (422)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <!-- This file lives in public/422.html -->
- <h1>422</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 422
+ </h1>
<h3>The change you requested was rejected.</h3>
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
diff --git a/public/500.html b/public/500.html
index 08c11bbd05a..3d59d1392f5 100644
--- a/public/500.html
+++ b/public/500.html
@@ -2,10 +2,50 @@
<html>
<head>
<title>Something went wrong (500)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <h1>500</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 500
+ </h1>
<h3>Whoops, something went wrong on our end.</h3>
<hr/>
<p>Try refreshing the page, or going back and attempting the action again.</p>
diff --git a/public/502.html b/public/502.html
index 9480a928439..67dfd8a2743 100644
--- a/public/502.html
+++ b/public/502.html
@@ -2,10 +2,50 @@
<html>
<head>
<title>GitLab is not responding (502)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <h1>502</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 502
+ </h1>
<h3>Whoops, GitLab is taking too much time to respond.</h3>
<hr/>
<p>Try refreshing the page, or going back and attempting the action again.</p>
diff --git a/public/deploy.html b/public/deploy.html
index 3822ed4b64d..48976dacf41 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -2,12 +2,49 @@
<html>
<head>
<title>Deploy in progress</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
<h1>
- <img src="/logo.svg" /><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
Deploy in progress
</h1>
<h3>Please try again in a few minutes.</h3>
diff --git a/public/logo.svg b/public/logo.svg
deleted file mode 100644
index fc4553137f7..00000000000
--- a/public/logo.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-<svg width="210" height="210" viewBox="0 0 210 210" xmlns="http://www.w3.org/2000/svg">
- <path d="M105.0614 203.655l38.64-118.921h-77.28l38.64 118.921z" fill="#e24329"/>
- <path d="M105.0614 203.6548l-38.64-118.921h-54.153l92.793 118.921z" fill="#fc6d26"/>
- <path d="M12.2685 84.7341l-11.742 36.139c-1.071 3.296.102 6.907 2.906 8.944l101.629 73.838-92.793-118.921z" fill="#fca326"/>
- <path d="M12.2685 84.7342h54.153l-23.273-71.625c-1.197-3.686-6.411-3.685-7.608 0l-23.272 71.625z" fill="#e24329"/>
- <path d="M105.0614 203.6548l38.64-118.921h54.153l-92.793 118.921z" fill="#fc6d26"/>
- <path d="M197.8544 84.7341l11.742 36.139c1.071 3.296-.102 6.907-2.906 8.944l-101.629 73.838 92.793-118.921z" fill="#fca326"/>
- <path d="M197.8544 84.7342h-54.153l23.273-71.625c1.197-3.686 6.411-3.685 7.608 0l23.272 71.625z" fill="#e24329"/>
-</svg>
diff --git a/public/static.css b/public/static.css
deleted file mode 100644
index 0a2b6060d48..00000000000
--- a/public/static.css
+++ /dev/null
@@ -1,36 +0,0 @@
-body {
- color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
- margin: auto;
- font-size: 14px;
-}
-
-h1 {
- font-size: 56px;
- line-height: 100px;
- font-weight: normal;
- color: #456;
-}
-
-h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
-}
-
-h3 {
- color: #456;
- font-size: 20px;
- font-weight: normal;
- line-height: 28px;
-}
-
-hr {
- margin: 18px 0;
- border: 0;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid white;
-}
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 8e06d4bdc77..98ae424ed7c 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -17,49 +17,79 @@ describe Projects::BranchesController do
describe "POST create" do
render_views
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- ref: ref
- end
+ context "on creation of a new branch" do
+ before do
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ ref: ref
+ end
- context "valid branch name, valid source" do
- let(:branch) { "merge_branch" }
- let(:ref) { "master" }
- it 'redirects' do
- expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/merge_branch")
+ context "valid branch name, valid source" do
+ let(:branch) { "merge_branch" }
+ let(:ref) { "master" }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/merge_branch")
+ end
+ end
+
+ context "invalid branch name, valid ref" do
+ let(:branch) { "<script>alert('merge');</script>" }
+ let(:ref) { "master" }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');")
+ end
+ end
+
+ context "valid branch name, invalid ref" do
+ let(:branch) { "merge_branch" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ end
+
+ context "invalid branch name, invalid ref" do
+ let(:branch) { "<script>alert('merge');</script>" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { "feature%2Ftest" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ it { project.repository.branch_names.include?('feature/test') }
end
end
- context "invalid branch name, valid ref" do
- let(:branch) { "<script>alert('merge');</script>" }
- let(:ref) { "master" }
+ describe "created from the new branch button on issues" do
+ let(:branch) { "1-feature-branch" }
+ let!(:issue) { create(:issue, project: project) }
+
+
it 'redirects' do
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');")
+ to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch")
end
- end
- context "valid branch name, invalid ref" do
- let(:branch) { "merge_branch" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- end
+ it 'posts a system note' do
+ expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch")
- context "invalid branch name, invalid ref" do
- let(:branch) { "<script>alert('merge');</script>" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- end
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ end
- context "valid branch name with encoded slashes" do
- let(:branch) { "feature%2Ftest" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- it { project.repository.branch_names.include?('feature/test')}
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 76d56bc989d..2cd81231144 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,16 +1,16 @@
require('spec_helper')
describe Projects::IssuesController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
+ describe "GET #index" do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
- describe "GET #index" do
it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path
@@ -38,6 +38,152 @@ describe Projects::IssuesController do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
+ end
+
+ describe 'Confidential Issues' do
+ let(:project) { create(:empty_project, :public) }
+ let(:assignee) { create(:assignee) }
+ let(:author) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
+ let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
+
+ describe 'GET #index' do
+ it 'should not list confidential issues for guests' do
+ sign_out(:user)
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
+ it 'should not list confidential issues for non project members' do
+ sign_in(non_member)
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
+ it 'should list confidential issues for author' do
+ sign_in(author)
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).not_to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for assignee' do
+ sign_in(assignee)
+ get_issues
+
+ expect(assigns(:issues)).not_to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for project members' do
+ sign_in(member)
+ project.team << [member, :developer]
+
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for admin' do
+ sign_in(admin)
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ def get_issues
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+ end
+ shared_examples_for 'restricted action' do |http_status|
+ it 'returns 404 for guests' do
+ sign_out :user
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
+ it 'returns 404 for non project members' do
+ sign_in(non_member)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
+ it "returns #{http_status[:success]} for author" do
+ sign_in(author)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for assignee" do
+ sign_in(assignee)
+ go(id: request_forgery_timing_attack.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for project members" do
+ sign_in(member)
+ project.team << [member, :developer]
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for admin" do
+ sign_in(admin)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+ end
+
+ describe 'GET #show' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+
+ describe 'GET #edit' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+
+ describe 'PUT #update' do
+ it_behaves_like 'restricted action', success: 302
+
+ def go(id:)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id,
+ issue: { title: 'New title' }
+ end
+ end
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 722095de590..e72aa9479b7 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -4,6 +4,10 @@ FactoryGirl.define do
author
project
+ trait :confidential do
+ confidential true
+ end
+
trait :closed do
state :closed
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index a9df5fa1d3a..e281e2f227b 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -51,6 +51,11 @@ FactoryGirl.define do
trait :with_diffs do
end
+ trait :without_diffs do
+ source_branch "improve/awesome"
+ target_branch "master"
+ end
+
trait :conflict do
source_branch "feature_conflict"
target_branch "feature"
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index bd85b1d798a..7ae06c27840 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -5,14 +5,15 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
FactoryGirl.define do
@@ -30,5 +31,10 @@ FactoryGirl.define do
trait :mentioned do
action { Todo::MENTIONED }
end
+
+ trait :on_commit do
+ commit_id RepoHelpers.sample_commit.id
+ target_type "Commit"
+ end
end
end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
new file mode 100644
index 00000000000..9219b767547
--- /dev/null
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Start new branch from an issue', feature: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ context "for team members" do
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ it 'shown the new branch button', js: false do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_link "New Branch"
+ end
+
+ context "when there is a referenced merge request" do
+ let(:note) do
+ create(:note, :on_issue, :system, project: project,
+ note: "mentioned in !#{referenced_mr.iid}")
+ end
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes ##{issue.iid}", author: user)
+ end
+
+ before do
+ issue.notes << note
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it "hides the new branch button", js: true do
+ expect(page).not_to have_link "New Branch"
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+ end
+
+ context "for visiters" do
+ it 'no button is shown', js: false do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(page).not_to have_link "New Branch"
+ end
+ end
+end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 1b2fd1bab10..b76e4c74c79 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -30,8 +30,6 @@ feature 'Merge Request filtering by Milestone', feature: true do
def filter_by_milestone(title)
find(".js-milestone-select").click
- sleep 0.5
find(".milestone-filter a", text: title).click
- sleep 1
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 57563add74c..f88c591d897 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -8,10 +8,12 @@ describe "Internal Project Access", feature: true do
let(:master) { create(:user) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
+ let(:external_team_member) { create(:user, external: true) }
before do
# full access
project.team << [master, :master]
+ project.team << [external_team_member, :master]
# readonly
project.team << [reporter, :reporter]
@@ -34,6 +36,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -45,6 +49,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -56,6 +62,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -67,6 +75,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -78,6 +88,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -89,22 +101,23 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/blob" do
- before do
- commit = project.repository.commit
- path = '.gitignore'
- @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path))
- end
+ let(:commit) { project.repository.commit }
+ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
- it { expect(@blob_path).to be_allowed_for master }
- it { expect(@blob_path).to be_allowed_for reporter }
- it { expect(@blob_path).to be_allowed_for :admin }
- it { expect(@blob_path).to be_allowed_for guest }
- it { expect(@blob_path).to be_allowed_for :user }
- it { expect(@blob_path).to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
+ it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/edit" do
@@ -115,6 +128,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -126,6 +141,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -137,6 +154,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -149,6 +168,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -160,6 +181,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -171,6 +194,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -182,6 +207,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -193,6 +220,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -209,6 +238,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -225,6 +256,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -236,6 +269,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index a1e111c6cab..19f287ce7a4 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -8,10 +8,12 @@ describe "Private Project Access", feature: true do
let(:master) { create(:user) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
+ let(:external_team_member) { create(:user, external: true) }
before do
# full access
project.team << [master, :master]
+ project.team << [external_team_member, :master]
# readonly
project.team << [reporter, :reporter]
@@ -34,6 +36,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -45,6 +49,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -56,6 +62,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -67,6 +75,7 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -78,6 +87,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -89,22 +100,23 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/blob" do
- before do
- commit = project.repository.commit
- path = '.gitignore'
- @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path))
- end
+ let(:commit) { project.repository.commit }
+ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))}
- it { expect(@blob_path).to be_allowed_for master }
- it { expect(@blob_path).to be_allowed_for reporter }
- it { expect(@blob_path).to be_allowed_for :admin }
- it { expect(@blob_path).to be_denied_for guest }
- it { expect(@blob_path).to be_denied_for :user }
- it { expect(@blob_path).to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
+ it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/edit" do
@@ -115,6 +127,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -126,6 +140,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -137,6 +153,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -149,6 +167,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -160,6 +180,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -171,6 +193,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -187,6 +211,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -203,6 +229,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
@@ -214,6 +242,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_allowed_for external_team_member }
it { is_expected.to be_denied_for :visitor }
end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index b98476f854e..4e135076367 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -38,6 +38,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -49,6 +50,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -60,6 +62,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -71,6 +74,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -82,6 +86,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -93,6 +98,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
@@ -107,6 +113,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -118,6 +125,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
end
@@ -135,6 +143,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -146,23 +155,22 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
end
describe "GET /:project_path/blob" do
- before do
- commit = project.repository.commit
- path = '.gitignore'
- @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path))
- end
+ let(:commit) { project.repository.commit }
+
+ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
- it { expect(@blob_path).to be_allowed_for master }
- it { expect(@blob_path).to be_allowed_for reporter }
- it { expect(@blob_path).to be_allowed_for :admin }
- it { expect(@blob_path).to be_allowed_for guest }
- it { expect(@blob_path).to be_allowed_for :user }
- it { expect(@blob_path).to be_allowed_for :visitor }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :visitor }
end
describe "GET /:project_path/edit" do
@@ -173,6 +181,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
@@ -184,6 +193,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
@@ -195,6 +205,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -207,6 +218,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
@@ -218,6 +230,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -229,6 +242,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
@@ -240,6 +254,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -251,6 +266,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
@@ -267,6 +283,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -283,6 +300,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
@@ -294,6 +312,7 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
end
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index e9bb388e361..9acf6304bcb 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -44,8 +44,78 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end
end
- context "for user references" do
+ context 'with data-issue' do
+ context 'for confidential issues' do
+ it 'removes references for non project members' do
+ non_member = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: non_member)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'allows references for author' do
+ author = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project, author: author)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: author)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for assignee' do
+ assignee = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project, assignee: assignee)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: assignee)
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for project members' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :developer]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: member)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for admin' do
+ admin = create(:admin)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: admin)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ it 'allows references for non confidential issues' do
+ user = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ context "for user references" do
context 'with data-group' do
it 'removes unpermitted Group references' do
user = create(:user)
diff --git a/spec/lib/ci/status_spec.rb b/spec/lib/ci/status_spec.rb
index 1539720bb8d..47f3df6e3ce 100644
--- a/spec/lib/ci/status_spec.rb
+++ b/spec/lib/ci/status_spec.rb
@@ -48,6 +48,29 @@ describe Ci::Status do
it { is_expected.to eq 'success' }
end
+ context 'success and canceled' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :canceled)]
+ end
+ it { is_expected.to eq 'failed' }
+ end
+
+ context 'all canceled' do
+ let(:statuses) do
+ [create(type, status: :canceled), create(type, status: :canceled)]
+ end
+ it { is_expected.to eq 'canceled' }
+ end
+
+ context 'success and canceled but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :canceled, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'success' }
+ end
+
context 'one finished and second running but allowed to fail' do
let(:statuses) do
[create(type, status: :success),
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 04cf11fc6f1..844fd79c991 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -11,6 +11,7 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
subject { described_class.new(project, project.creator) }
before do
+ project.team << [project.creator, :developer]
project2.team << [project.creator, :master]
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0d9694f2c13..a0cbef6e6a4 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -18,4 +18,18 @@ describe Gitlab::Diff::File, lib: true do
describe :mode_changed? do
it { expect(diff_file.mode_changed?).to be_falsey }
end
+
+ describe '#too_large?' do
+ it 'returns true for a file that is too large' do
+ expect(diff).to receive(:too_large?).and_return(true)
+
+ expect(diff_file.too_large?).to eq(true)
+ end
+
+ it 'returns false for a file that is small enough' do
+ expect(diff).to receive(:too_large?).and_return(false)
+
+ expect(diff_file.too_large?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 09adbc07dcb..db0ff95b4f5 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe Gitlab::ProjectSearchResults, lib: true do
+ let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') }
+ let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
@@ -14,10 +15,74 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) }
+ let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
end
+
+ describe 'confidential issues' do
+ let(:query) { 'issue' }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+
+ it 'should not list project confidential issues for non project members' do
+ results = described_class.new(non_member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 1
+ end
+
+ it 'should list project confidential issues for author' do
+ results = described_class.new(author, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 2
+ end
+
+ it 'should list project confidential issues for assignee' do
+ results = described_class.new(assignee, project.id, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 2
+ end
+
+ it 'should list project confidential issues for project members' do
+ project.team << [member, :developer]
+
+ results = described_class.new(member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list all project issues for admin' do
+ results = described_class.new(admin, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 3
+ end
+ end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7d963795e17..65af37e24f1 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
+
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
it 'accesses valid user objects' do
@@ -41,6 +42,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid issue objects' do
+ project.team << [project.creator, :developer]
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index bb18f417858..f4afe597e8d 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Gitlab::SearchResults do
+ let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
@@ -9,7 +10,7 @@ describe Gitlab::SearchResults do
end
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
- let(:results) { described_class.new(Project.all, 'foo') }
+ let(:results) { described_class.new(user, Project.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
@@ -52,4 +53,92 @@ describe Gitlab::SearchResults do
expect(results.empty?).to eq(false)
end
end
+
+ describe 'confidential issues' do
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
+ let(:project_3) { create(:empty_project) }
+ let(:project_4) { create(:empty_project) }
+ let(:query) { 'issue' }
+ let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) }
+ let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) }
+ let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
+ let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
+
+ it 'should not list confidential issues for non project members' do
+ results = described_class.new(non_member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 1
+ end
+
+ it 'should list confidential issues for author' do
+ results = described_class.new(author, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list confidential issues for assignee' do
+ results = described_class.new(assignee, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list confidential issues for project members' do
+ project_1.team << [member, :developer]
+ project_2.team << [member, :developer]
+
+ results = described_class.new(member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 4
+ end
+
+ it 'should list all issues for admin' do
+ results = described_class.new(admin, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 5
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 253902512c3..0e9111c8029 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -86,10 +86,21 @@ eos
let(:issue) { create :issue, project: project }
let(:other_project) { create :project, :public }
let(:other_issue) { create :issue, project: other_project }
+ let(:commiter) { create :user }
+
+ before do
+ project.team << [commiter, :developer]
+ other_project.team << [commiter, :developer]
+ end
it 'detects issues that this commit is marked as closing' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
- allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}")
+
+ allow(commit).to receive_messages(
+ safe_message: "Fixes ##{issue.iid} and #{ext_ref}",
+ committer_email: commiter.email
+ )
+
expect(commit.closes_issues).to include(issue)
expect(commit.closes_issues).to include(other_issue)
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 20f0c561e44..cb33edde820 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -48,7 +48,8 @@ describe Issue, "Mentionable" do
describe '#create_new_cross_references!' do
let(:project) { create(:project) }
- let(:issues) { create_list(:issue, 2, project: project) }
+ let(:author) { create(:author) }
+ let(:issues) { create_list(:issue, 2, project: project, author: author) }
context 'before changes are persisted' do
it 'ignores pre-existing references' do
@@ -91,7 +92,7 @@ describe Issue, "Mentionable" do
end
def create_issue(description:)
- create(:issue, project: project, description: description)
+ create(:issue, project: project, description: description, author: author)
end
end
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
new file mode 100644
index 00000000000..47c3be673c5
--- /dev/null
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Milestone, 'Milestoneish' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
+ let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
+ let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
+ let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
+ let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
+ let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
+
+ before do
+ project.team << [member, :developer]
+ end
+
+ describe '#closed_items_count' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.closed_items_count(non_member)).to eq 2
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.closed_items_count(author)).to eq 4
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.closed_items_count(assignee)).to eq 4
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.closed_items_count(member)).to eq 6
+ end
+
+ it 'should count all issues for admin' do
+ expect(milestone.closed_items_count(admin)).to eq 6
+ end
+ end
+
+ describe '#total_items_count' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.total_items_count(non_member)).to eq 4
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.total_items_count(author)).to eq 7
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.total_items_count(assignee)).to eq 7
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.total_items_count(member)).to eq 10
+ end
+
+ it 'should count all issues for admin' do
+ expect(milestone.total_items_count(admin)).to eq 10
+ end
+ end
+
+ describe '#complete?' do
+ it 'returns false when has items opened' do
+ expect(milestone.complete?(non_member)).to eq false
+ end
+
+ it 'returns true when all items are closed' do
+ issue.close
+ merge_request.close
+
+ expect(milestone.complete?(non_member)).to eq true
+ end
+ end
+
+ describe '#percent_complete' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.percent_complete(non_member)).to eq 50
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.percent_complete(author)).to eq 57
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.percent_complete(assignee)).to eq 57
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.percent_complete(member)).to eq 60
+ end
+
+ it 'should count confidential issues for admin' do
+ expect(milestone.percent_complete(admin)).to eq 60
+ end
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index ec2a923f91b..5fe44246738 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -65,6 +65,42 @@ describe Event, models: true do
it { expect(@event.author).to eq(@user) }
end
+ describe '#proper?' do
+ context 'issue event' do
+ let(:project) { create(:empty_project, :public) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:event) { Event.new(project: project, action: Event::CREATED, target: issue, author_id: author.id) }
+
+ before do
+ project.team << [member, :developer]
+ end
+
+ context 'for non confidential issues' do
+ let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
+
+ it { expect(event.proper?(non_member)).to eq true }
+ it { expect(event.proper?(author)).to eq true }
+ it { expect(event.proper?(assignee)).to eq true }
+ it { expect(event.proper?(member)).to eq true }
+ it { expect(event.proper?(admin)).to eq true }
+ end
+
+ context 'for confidential issues' do
+ let(:issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+
+ it { expect(event.proper?(non_member)).to eq false }
+ it { expect(event.proper?(author)).to eq true }
+ it { expect(event.proper?(assignee)).to eq true }
+ it { expect(event.proper?(member)).to eq true }
+ it { expect(event.proper?(admin)).to eq true }
+ end
+ end
+ end
+
describe '.limit_recent' do
let!(:event1) { create(:closed_issue_event) }
let!(:event2) { create(:closed_issue_event) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 7f44ca2f7db..540a62eb1f8 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -130,6 +130,15 @@ describe Issue, models: true do
end
end
+ describe '#related_branches' do
+ it "selects the right branches" do
+ allow(subject.project.repository).to receive(:branch_names).
+ and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name])
+
+ expect(subject.related_branches).to eq [subject.to_branch_name]
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:issue) }
@@ -140,4 +149,12 @@ describe Issue, models: true do
it_behaves_like 'a Taskable' do
let(:subject) { create :issue }
end
+
+ describe "#to_branch_name" do
+ let(:issue) { create(:issue, title: 'a' * 30) }
+
+ it "starts with the issue iid" do
+ expect(issue.to_branch_name).to match /-#{issue.iid}\z/
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8bf68013fd2..2165cfb7a32 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -86,6 +86,41 @@ describe MergeRequest, models: true do
end
end
+ describe '#target_sha' do
+ context 'when the target branch does not exist anymore' do
+ subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } }
+
+ it 'returns nil' do
+ expect(subject.target_sha).to be_nil
+ end
+ end
+ end
+
+ describe '#source_sha' do
+ let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
+
+ context 'with diffs' do
+ subject { create(:merge_request, :with_diffs) }
+ it 'returns the sha of the source branch last commit' do
+ expect(subject.source_sha).to eq(last_branch_commit.sha)
+ end
+ end
+
+ context 'without diffs' do
+ subject { create(:merge_request, :without_diffs) }
+ it 'returns the sha of the source branch last commit' do
+ expect(subject.source_sha).to eq(last_branch_commit.sha)
+ end
+ end
+
+ context 'when the merge request is being created' do
+ subject { build(:merge_request, source_branch: nil, compare_commits: []) }
+ it 'returns nil' do
+ expect(subject.source_sha).to be_nil
+ end
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
@@ -150,6 +185,7 @@ describe MergeRequest, models: true do
let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
before do
+ subject.project.team << [subject.author, :developer]
allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
end
@@ -284,6 +320,18 @@ describe MergeRequest, models: true do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
+ context 'when the target branch does not exist anymore' do
+ subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } }
+
+ it 'does not crash' do
+ expect{ subject.diverged_commits_count }.not_to raise_error
+ end
+
+ it 'returns 0' do
+ expect(subject.diverged_commits_count).to eq(0)
+ end
+ end
+
context 'diverged on same repository' do
subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index de1757bf67a..72a4ea70228 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -32,6 +32,7 @@ describe Milestone, models: true do
let(:milestone) { create(:milestone) }
let(:issue) { create(:issue) }
+ let(:user) { create(:user) }
describe "unique milestone title per project" do
it "shouldn't accept the same title in a project twice" do
@@ -50,18 +51,17 @@ describe Milestone, models: true do
describe "#percent_complete" do
it "should not count open issues" do
milestone.issues << issue
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
it "should count closed issues" do
issue.close
milestone.issues << issue
- expect(milestone.percent_complete).to eq(100)
+ expect(milestone.percent_complete(user)).to eq(100)
end
it "should recover from dividing by zero" do
- expect(milestone.issues).to receive(:size).and_return(0)
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
end
@@ -103,7 +103,7 @@ describe Milestone, models: true do
)
end
- it { expect(milestone.percent_complete).to eq(75) }
+ it { expect(milestone.percent_complete(user)).to eq(75) }
end
describe :items_count do
@@ -113,23 +113,23 @@ describe Milestone, models: true do
milestone.merge_requests << create(:merge_request)
end
- it { expect(milestone.closed_items_count).to eq(1) }
- it { expect(milestone.total_items_count).to eq(3) }
- it { expect(milestone.is_empty?).to be_falsey }
+ it { expect(milestone.closed_items_count(user)).to eq(1) }
+ it { expect(milestone.total_items_count(user)).to eq(3) }
+ it { expect(milestone.is_empty?(user)).to be_falsey }
end
describe :can_be_closed? do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe :is_empty? do
+ describe :total_items_count do
before do
create :closed_issue, milestone: milestone
create :merge_request, milestone: milestone
end
it 'Should return total count of issues and merge requests assigned to milestone' do
- expect(milestone.total_items_count).to eq 2
+ expect(milestone.total_items_count(user)).to eq 2
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index fc2ab2d9931..a57229a4fdf 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -597,9 +597,9 @@ describe Repository, models: true do
describe '#after_push_commit' do
it 'flushes the cache' do
- expect(repository).to receive(:expire_cache).with('master')
+ expect(repository).to receive(:expire_cache).with('master', '123')
- repository.after_push_commit('master')
+ repository.after_push_commit('master', '123')
end
end
@@ -703,4 +703,111 @@ describe Repository, models: true do
repository.rm_tag('8.5')
end
end
+
+ describe '#avatar' do
+ it 'returns the first avatar file found in the repository' do
+ expect(repository).to receive(:blob_at_branch).
+ with('master', 'logo.png').
+ and_return(true)
+
+ expect(repository.avatar).to eq('logo.png')
+ end
+
+ it 'caches the output' do
+ allow(repository).to receive(:blob_at_branch).
+ with('master', 'logo.png').
+ and_return(true)
+
+ expect(repository.avatar).to eq('logo.png')
+
+ expect(repository).to_not receive(:blob_at_branch)
+ expect(repository.avatar).to eq('logo.png')
+ end
+ end
+
+ describe '#expire_avatar_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ before do
+ allow(repository).to receive(:cache).and_return(cache)
+ end
+
+ context 'without a branch or revision' do
+ it 'flushes the cache' do
+ expect(cache).to receive(:expire).with(:avatar)
+
+ repository.expire_avatar_cache
+ end
+ end
+
+ context 'with a branch' do
+ it 'does not flush the cache if the branch is not the default branch' do
+ expect(cache).not_to receive(:expire)
+
+ repository.expire_avatar_cache('cats')
+ end
+
+ it 'flushes the cache if the branch equals the default branch' do
+ expect(cache).to receive(:expire).with(:avatar)
+
+ repository.expire_avatar_cache(repository.root_ref)
+ end
+ end
+
+ context 'with a branch and revision' do
+ let(:commit) { double(:commit) }
+
+ before do
+ allow(repository).to receive(:commit).and_return(commit)
+ end
+
+ it 'does not flush the cache if the commit does not change any logos' do
+ diff = double(:diff, new_path: 'test.txt')
+
+ expect(commit).to receive(:diffs).and_return([diff])
+ expect(cache).not_to receive(:expire)
+
+ repository.expire_avatar_cache(repository.root_ref, '123')
+ end
+
+ it 'flushes the cache if the commit changes any of the logos' do
+ diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
+
+ expect(commit).to receive(:diffs).and_return([diff])
+ expect(cache).to receive(:expire).with(:avatar)
+
+ repository.expire_avatar_cache(repository.root_ref, '123')
+ end
+ end
+ end
+
+ describe '#build_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'builds the caches if they do not already exist' do
+ expect(cache).to receive(:exist?).
+ exactly(repository.cache_keys.length).
+ times.
+ and_return(false)
+
+ repository.cache_keys.each do |key|
+ expect(repository).to receive(key)
+ end
+
+ repository.build_cache
+ end
+
+ it 'does not build any caches that already exist' do
+ expect(cache).to receive(:exist?).
+ exactly(repository.cache_keys.length).
+ times.
+ and_return(true)
+
+ repository.cache_keys.each do |key|
+ expect(repository).to_not receive(key)
+ end
+
+ repository.build_cache
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index fe9ea7e7d1e..d9b86b9368f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -5,19 +5,24 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
require 'spec_helper'
describe Todo, models: true do
+ let(:project) { create(:project) }
+ let(:commit) { project.commit }
+ let(:issue) { create(:issue) }
+
describe 'relationships' do
it { is_expected.to belong_to(:author).class_name("User") }
it { is_expected.to belong_to(:note) }
@@ -33,8 +38,22 @@ describe Todo, models: true do
describe 'validations' do
it { is_expected.to validate_presence_of(:action) }
- it { is_expected.to validate_presence_of(:target) }
+ it { is_expected.to validate_presence_of(:target_type) }
it { is_expected.to validate_presence_of(:user) }
+
+ context 'for commits' do
+ subject { described_class.new(target_type: 'Commit') }
+
+ it { is_expected.to validate_presence_of(:commit_id) }
+ it { is_expected.not_to validate_presence_of(:target_id) }
+ end
+
+ context 'for issuables' do
+ subject { described_class.new(target: issue) }
+
+ it { is_expected.to validate_presence_of(:target_id) }
+ it { is_expected.not_to validate_presence_of(:commit_id) }
+ end
end
describe '#body' do
@@ -55,15 +74,69 @@ describe Todo, models: true do
end
end
- describe '#done!' do
+ describe '#done' do
it 'changes state to done' do
todo = create(:todo, state: :pending)
- expect { todo.done! }.to change(todo, :state).from('pending').to('done')
+ expect { todo.done }.to change(todo, :state).from('pending').to('done')
end
it 'does not raise error when is already done' do
todo = create(:todo, state: :done)
- expect { todo.done! }.not_to raise_error
+ expect { todo.done }.not_to raise_error
+ end
+ end
+
+ describe '#for_commit?' do
+ it 'returns true when target is a commit' do
+ subject.target_type = 'Commit'
+ expect(subject.for_commit?).to eq true
+ end
+
+ it 'returns false when target is an issuable' do
+ subject.target_type = 'Issue'
+ expect(subject.for_commit?).to eq false
+ end
+ end
+
+ describe '#target' do
+ context 'for commits' do
+ it 'returns an instance of Commit when exists' do
+ subject.project = project
+ subject.target_type = 'Commit'
+ subject.commit_id = commit.id
+
+ expect(subject.target).to be_a(Commit)
+ expect(subject.target).to eq commit
+ end
+
+ it 'returns nil when does not exists' do
+ subject.project = project
+ subject.target_type = 'Commit'
+ subject.commit_id = 'xxxx'
+
+ expect(subject.target).to be_nil
+ end
+ end
+
+ it 'returns the issuable for issuables' do
+ subject.target_id = issue.id
+ subject.target_type = issue.class.name
+ expect(subject.target).to eq issue
+ end
+ end
+
+ describe '#target_reference' do
+ it 'returns the short commit id for commits' do
+ subject.project = project
+ subject.target_type = 'Commit'
+ subject.commit_id = commit.id
+
+ expect(subject.target_reference).to eq commit.short_id
+ end
+
+ it 'returns reference for issuables' do
+ subject.target = issue
+ expect(subject.target_reference).to eq issue.to_reference
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6290ab3ebec..0ab7fd88ce6 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -180,6 +180,20 @@ describe User, models: true do
it { is_expected.to respond_to(:is_admin?) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:private_token) }
+ it { is_expected.to respond_to(:external?) }
+ end
+
+ describe 'before save hook' do
+ context 'when saving an external user' do
+ let(:user) { create(:user) }
+ let(:external_user) { create(:user, external: true) }
+
+ it "sets other properties aswell" do
+ expect(external_user.can_create_team).to be_falsey
+ expect(external_user.can_create_group).to be_falsey
+ expect(external_user.projects_limit).to be 0
+ end
+ end
end
describe '#confirm' do
@@ -404,6 +418,7 @@ describe User, models: true do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
+ expect(user.external).to be_falsey
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 571ea2dae4c..bb2ab058003 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -3,7 +3,11 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
+ let(:non_member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:admin) }
+ let!(:project) { create(:project, :public, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -12,6 +16,13 @@ describe API::API, api: true do
state: :closed,
milestone: milestone
end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee
+ end
let!(:issue) do
create :issue,
author: user,
@@ -123,10 +134,43 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
- it "should return project issues" do
+ it 'should return project issues without confidential issues for non project members' do
+ get api("#{base_url}/issues", non_member)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for author' do
+ get api("#{base_url}/issues", author)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for assignee' do
+ get api("#{base_url}/issues", assignee)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for admin' do
+ get api("#{base_url}/issues", admin)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
@@ -206,6 +250,41 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/54321", user)
expect(response.status).to eq(404)
end
+
+ context 'confidential issues' do
+ it "should return 404 for non project members" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+ expect(response.status).to eq(404)
+ end
+
+ it "should return confidential issue for project members" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for author" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for assignee" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for admin" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
end
describe "POST /projects/:id/issues" do
@@ -294,6 +373,35 @@ describe API::API, api: true do
expect(response.status).to eq(400)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
end
+
+ context 'confidential issues' do
+ it "should return 403 for non project members" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+ expect(response.status).to eq(403)
+ end
+
+ it "should update a confidential issue for project members" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "should update a confidential issue for author" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "should update a confidential issue for admin" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+ end
end
describe 'PUT /projects/:id/issues/:issue_id to update labels' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 96e8c8c51f8..679227bf881 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -120,6 +120,26 @@ describe API::API, api: true do
expect(response.status).to eq(201)
end
+ it 'creates non-external users by default' do
+ post api("/users", admin), attributes_for(:user)
+ expect(response.status).to eq(201)
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+ expect(new_user).not_to eq nil
+ expect(new_user.external).to be_falsy
+ end
+
+ it 'should allow an external user to be created' do
+ post api("/users", admin), attributes_for(:user, external: true)
+ expect(response.status).to eq(201)
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+ expect(new_user).not_to eq nil
+ expect(new_user.external).to be_truthy
+ end
+
it "should not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
@@ -262,6 +282,13 @@ describe API::API, api: true do
expect(user.reload.admin).to eq(true)
end
+ it "should update external status" do
+ put api("/users/#{user.id}", admin), { external: true }
+ expect(response.status).to eq 200
+ expect(json_response['external']).to eq(true)
+ expect(user.reload.external?).to be_truthy
+ end
+
it "should not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
expect(response.status).to eq(200)
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 145bc937560..8490a729e51 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -29,7 +29,8 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy }
it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache).with('master')
+ expect(project.repository).to receive(:expire_cache).
+ with('master', newrev)
subject
end
@@ -46,7 +47,8 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy }
it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache).with('master')
+ expect(project.repository).to receive(:expire_cache).
+ with('master', newrev)
subject
end
@@ -65,7 +67,8 @@ describe GitPushService, services: true do
end
it 'flushes general cached data' do
- expect(project.repository).to receive(:expire_cache).with('master')
+ expect(project.repository).to receive(:expire_cache).
+ with('master', newrev)
subject
end
@@ -212,12 +215,16 @@ describe GitPushService, services: true do
let(:commit) { project.commit }
before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email
)
+
allow(project.repository).to receive(:commits_between).and_return([commit])
end
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
new file mode 100644
index 00000000000..6108c26a78b
--- /dev/null
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Projects::AutocompleteService, services: true do
+ describe '#issues' do
+ describe 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, :public) }
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+
+ it 'should not list project confidential issues for guests' do
+ autocomplete = described_class.new(project, nil)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
+ it 'should not list project confidential issues for non project members' do
+ autocomplete = described_class.new(project, non_member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
+ it 'should list project confidential issues for author' do
+ autocomplete = described_class.new(project, author)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 2
+ end
+
+ it 'should list project confidential issues for assignee' do
+ autocomplete = described_class.new(project, assignee)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 2
+ end
+
+ it 'should list project confidential issues for project members' do
+ project.team << [member, :developer]
+
+ autocomplete = described_class.new(project, member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 3
+ end
+
+ it 'should list all project issues for admin' do
+ autocomplete = described_class.new(project, admin)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 3
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 93bf1b81fbe..4c5ced7e746 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -12,7 +12,7 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
- expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
+ expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
subject.execute
expect(project.pushes_since_gc).to eq(0)
@@ -20,7 +20,7 @@ describe Projects::HousekeepingService do
it 'does not enqueue a job when no lease can be obtained' do
expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(GitlabShellWorker).not_to receive(:perform_async)
+ expect(GitlabShellOneShotWorker).not_to receive(:perform_async)
expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
expect(project.pushes_since_gc).to eq(0)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5dcc39f5fdc..8e6292014d4 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -280,6 +280,18 @@ describe SystemNoteService, services: true do
end
end
+ describe '.new_issue_branch' do
+ subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") }
+
+ it_behaves_like 'a system note'
+
+ context 'when a branch is created from the new branch button' do
+ it 'sets the note text' do
+ expect(subject.note).to match /\AStarted branch [`1-mepmep`]/
+ end
+ end
+ end
+
describe '.cross_reference' do
subject { described_class.cross_reference(noteable, mentioner, author) }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 96420acb31d..b4728807b8b 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -148,8 +148,13 @@ describe TodoService, services: true do
should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
- it 'does not create todo when leaving a note on commit' do
- should_not_create_any_todo { service.new_note(note_on_commit, john_doe) }
+ it 'creates a todo for each valid mentioned user when leaving a note on commit' do
+ service.new_note(note_on_commit, john_doe)
+
+ should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
it 'does not create todo when leaving a note on snippet' do
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 558e8b1612f..4e007c777e3 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -15,6 +15,8 @@ module AccessMatchers
logout
when :admin
login_as(create(:admin))
+ when :external
+ login_as(create(:user, external: true))
when User
login_as(user)
else
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index fce91015fd4..e876d44c166 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -52,6 +52,8 @@ shared_context 'mentionable context' do
end
set_mentionable_text.call(ref_string)
+
+ project.team << [author, :developer]
end
end