summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG13
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js.coffee61
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js.coffee35
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee5
-rw-r--r--app/assets/javascripts/blob/template_selector.js.coffee56
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee24
-rw-r--r--app/assets/javascripts/merge_request.js.coffee2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss32
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss11
-rw-r--r--app/assets/stylesheets/framework/variables.scss10
-rw-r--r--app/assets/stylesheets/mailers/devise.scss4
-rw-r--r--app/assets/stylesheets/pages/editor.scss3
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/pages/profile.scss19
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/controllers/application_controller.rb17
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb42
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb38
-rw-r--r--app/models/jira_issue.rb2
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/project.rb18
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/ci/create_builds_service.rb54
-rw-r--r--app/services/ci/create_pipeline_service.rb24
-rw-r--r--app/services/create_commit_builds_service.rb55
-rw-r--r--app/services/projects/autocomplete_service.rb4
-rw-r--r--app/views/devise/mailer/password_change.html.haml10
-rw-r--r--app/views/devise/mailer/password_change.text.erb7
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.erb8
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.haml12
-rw-r--r--app/views/devise/mailer/reset_password_instructions.text.erb10
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml19
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb7
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml105
-rw-r--r--app/views/projects/_last_push.html.haml22
-rw-r--r--app/views/projects/blob/_editor.html.haml10
-rw-r--r--app/views/projects/builds/show.html.haml10
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--app/views/projects/tree/_blob_item.html.haml5
-rw-r--r--app/views/projects/tree/_tree_item.html.haml6
-rw-r--r--config/initializers/default_url_options.rb1
-rw-r--r--config/routes.rb7
-rw-r--r--db/migrate/20160415062917_create_personal_access_tokens.rb13
-rw-r--r--db/schema.rb14
-rw-r--r--doc/api/README.md61
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb51
-rw-r--r--lib/banzai/filter/external_link_filter.rb13
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb31
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb15
-rw-r--r--lib/gitlab/ci/config/node/validation_helpers.rb17
-rw-r--r--spec/controllers/application_controller_spec.rb71
-rw-r--r--spec/factories/personal_access_tokens.rb9
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb94
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb14
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb12
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb1
-rw-r--r--spec/finders/notes_finder_spec.rb7
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb6
-rw-r--r--spec/javascripts/merge_request_spec.js.coffee2
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb52
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb34
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb9
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb115
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/mailers/previews/devise_mailer_preview.rb23
-rw-r--r--spec/models/ci/pipeline_spec.rb13
-rw-r--r--spec/models/jira_issue_spec.rb30
-rw-r--r--spec/models/personal_access_token_spec.rb15
-rw-r--r--spec/models/project_services/jira_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb18
-rw-r--r--spec/requests/api/api_helpers_spec.rb76
-rw-r--r--spec/services/ci/create_builds_service_spec.rb6
-rw-r--r--spec/services/create_commit_builds_service_spec.rb23
-rw-r--r--spec/services/git_push_service_spec.rb3
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb12
90 files changed, 1359 insertions, 400 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 75b48ad3207..b12ba9f31b1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,8 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
+ - Fix error when CI job variables key specified but not defined
+ - Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
@@ -12,6 +14,7 @@ v 8.9.0 (unreleased)
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
- Make EmailsOnPushWorker use Sidekiq mailers queue
+ - Redesign all Devise emails. !4297
- Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
@@ -28,6 +31,7 @@ v 8.9.0 (unreleased)
- Add a metric for the number of new Redis connections created by a transaction
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages
+ - Fix images in sign-up confirmation email
- Added shortcut 'y' for copying a files content hash URL #14470
- Fix groups API to list only user's accessible projects
- Fix horizontal scrollbar for long commit message.
@@ -38,7 +42,9 @@ v 8.9.0 (unreleased)
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0
+ - Fixed alignment of download dropdown in merge requests
- Upgrade to jQuery 2
+ - Adds selected branch name to the dropdown toggle
- Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load
@@ -54,16 +60,20 @@ v 8.9.0 (unreleased)
- Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
- TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged'
+ - Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
- Toggle whitespace button now available for compare branches diffs #17881
- Pipelines can be canceled only when there are running builds
+ - Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
+ - Added Gfm autocomplete for labels
- Make Omniauth providers specs to not modify global configuration
+ - Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
@@ -108,6 +118,8 @@ v 8.9.0 (unreleased)
- Include user relationships when retrieving award_emoji
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
- Set inverse_of for Project/Service association to reduce the number of queries
+ - Update tanuki logo highlight/loading colors
+ - Use Git cached counters for branches and tags on project page
v 8.8.5
- Import GitHub repositories respecting the API rate limit !4166
@@ -120,6 +132,7 @@ v 8.8.5
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
- Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
+ - Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493
diff --git a/Gemfile b/Gemfile
index 2b508cf528d..bc1223e1bbc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -52,7 +52,7 @@ gem "browser", '~> 2.0.3'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 10.0'
+gem "gitlab_git", '~> 10.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
diff --git a/Gemfile.lock b/Gemfile.lock
index 3c599d04eda..49e548fb94f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -277,7 +277,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (10.1.3)
+ gitlab_git (10.2.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -874,7 +874,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0)
- gitlab_git (~> 10.0)
+ gitlab_git (~> 10.2)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
index cc8a497d081..8d0e3f363d1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
@@ -1,58 +1,5 @@
-class @BlobGitignoreSelector
- constructor: (opts) ->
- {
- @dropdown
- @editor
- @$wrapper = @dropdown.closest('.gitignore-selector')
- @$filenameInput = $('#file_name')
- @data = @dropdown.data('filenames')
- } = opts
+#= require blob/template_selector
- @dropdown.glDropdown(
- data: @data,
- filterable: true,
- selectable: true,
- search:
- fields: ['name']
- clicked: @onClick
- text: (gitignore) ->
- gitignore.name
- )
-
- @toggleGitignoreSelector()
- @bindEvents()
-
- bindEvents: ->
- @$filenameInput
- .on 'keyup blur', (e) =>
- @toggleGitignoreSelector()
-
- toggleGitignoreSelector: ->
- filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
- @$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
-
- onClick: (item, el, e) =>
- e.preventDefault()
- @requestIgnoreFile(item.name)
-
- requestIgnoreFile: (name) ->
- Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
-
- requestIgnoreFileSuccess: (gitignore) ->
- @editor.setValue(gitignore.content, 1)
- @editor.focus()
-
-class @BlobGitignoreSelectors
- constructor: (opts) ->
- {
- @$dropdowns = $('.js-gitignore-selector')
- @editor
- } = opts
-
- @$dropdowns.each (i, dropdown) =>
- $dropdown = $(dropdown)
-
- new BlobGitignoreSelector(
- dropdown: $dropdown,
- editor: @editor
- )
+class @BlobGitignoreSelector extends TemplateSelector
+ requestFile: (query) ->
+ Api.gitignoreText query.name, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
new file mode 100644
index 00000000000..a719ba25122
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
@@ -0,0 +1,17 @@
+class @BlobGitignoreSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitignore-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobGitignoreSelector(
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee
index e17eaa75dc1..a3cc8dd844c 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js.coffee
+++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee
@@ -1,30 +1,9 @@
-class @BlobLicenseSelector
- licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
+#= require blob/template_selector
- constructor: (editor) ->
- @$licenseSelector = $('.js-license-selector')
- $fileNameInput = $('#file_name')
+class @BlobLicenseSelector extends TemplateSelector
+ requestFile: (query) ->
+ data =
+ project: @dropdown.data('project')
+ fullname: @dropdown.data('fullname')
- initialFileNameValue = if $fileNameInput.length
- $fileNameInput.val()
- else if $('.editor-file-name').length
- $('.editor-file-name').text().trim()
-
- @toggleLicenseSelector(initialFileNameValue)
-
- if $fileNameInput
- $fileNameInput.on 'keyup blur', (e) =>
- @toggleLicenseSelector($(e.target).val())
-
- $('select.license-select').on 'change', (e) ->
- data =
- project: $(this).data('project')
- fullname: $(this).data('fullname')
- Api.licenseText $(this).val(), data, (license) ->
- editor.setValue(license.content, -1)
-
- toggleLicenseSelector: (fileName) =>
- if @licenseRegex.test(fileName)
- @$licenseSelector.show()
- else
- @$licenseSelector.hide()
+ Api.licenseText query.id, data, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
new file mode 100644
index 00000000000..68438733108
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
@@ -0,0 +1,17 @@
+class @BlobLicenseSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-license-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobLicenseSelector(
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 79141e768b8..636f909dbd0 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -12,8 +12,9 @@ class @EditBlob
$("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
- new BlobLicenseSelector(@editor)
- new BlobGitignoreSelectors(editor: @editor)
+
+ new BlobLicenseSelectors { @editor }
+ new BlobGitignoreSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee
new file mode 100644
index 00000000000..e76e303189d
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js.coffee
@@ -0,0 +1,56 @@
+class @TemplateSelector
+ constructor: (opts = {}) ->
+ {
+ @dropdown,
+ @data,
+ @pattern,
+ @wrapper,
+ @editor,
+ @fileEndpoint,
+ @$input = $('#file_name')
+ } = opts
+
+ @buildDropdown()
+ @bindEvents()
+ @onFilenameUpdate()
+
+ buildDropdown: ->
+ @dropdown.glDropdown(
+ data: @data,
+ filterable: true,
+ selectable: true,
+ search:
+ fields: ['name']
+ clicked: @onClick
+ text: (item) ->
+ item.name
+ )
+
+ bindEvents: ->
+ @$input.on('keyup blur', (e) =>
+ @onFilenameUpdate()
+ )
+
+ onFilenameUpdate: ->
+ return unless @$input.length
+
+ filenameMatches = @pattern.test(@$input.val().trim())
+
+ if not filenameMatches
+ @wrapper.addClass('hidden')
+ return
+
+ @wrapper.removeClass('hidden')
+
+ onClick: (item, el, e) =>
+ e.preventDefault()
+ @requestFile(item)
+
+ requestFile: (item) ->
+ # To be implemented on the extending class
+ # e.g.
+ # Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+
+ requestFileSuccess: (file) ->
+ @editor.setValue(file.content, 1)
+ @editor.focus()
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 76c3083232b..190bb38504c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -15,6 +15,9 @@ GitLab.GfmAutoComplete =
Members:
template: '<li>${username} <small>${title}</small></li>'
+ Labels:
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+
# Issues and MergeRequests
Issues:
template: '<li><small>${id}</small> ${title}</li>'
@@ -176,6 +179,25 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
+ @input.atwho
+ at: '~'
+ alias: 'labels'
+ searchKey: 'search'
+ displayTpl: @Labels.template
+ insertTpl: '${atwho-at}${title}'
+ callbacks:
+ beforeSave: (merges) ->
+ sanitizeLabelTitle = (title)->
+ if /\w+\s+\w+/g.test(title)
+ "\"#{sanitize(title)}\""
+ else
+ sanitize(title)
+
+ $.map merges, (m) ->
+ title: sanitizeLabelTitle(m.title)
+ color: m.color
+ search: "#{m.title}"
+
destroyAtWho: ->
@input.atwho('destroy')
@@ -195,6 +217,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
+ # load labels
+ @input.atwho 'load', '~', data.labels
# This trigger at.js again
# otherwise we would be stuck with loading until the user types
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 1f46e331427..dabfd91cf14 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -9,7 +9,7 @@ class @MergeRequest
# Options:
# action - String, current controller action
#
- constructor: (@opts) ->
+ constructor: (@opts = {}) ->
this.$el = $('.merge-request')
this.$('.show-all-commits').on 'click', =>
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 49a4727205a..894f80586f1 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -88,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) ->
if window.location.hash
- navBarHeight = $('.navbar-gitlab').outerHeight()
+ navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index fab96404a6c..d5fe5bc2ef1 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -91,6 +91,10 @@
background-color: $white-light;
border-top: none;
}
+
+ &.top-block .container-fluid {
+ background-color: inherit;
+ }
}
.cover-block {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index dca4dbb9f7d..a7bcb456560 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,6 +2,17 @@
* Application Header
*
*/
+@mixin tanuki-logo-colors($path-color) {
+ fill: $path-color;
+ transition: all 0.8s;
+
+ &:hover,
+ &.highlight {
+ fill: lighten($path-color, 25%);
+ transition: all 0.1s;
+ }
+}
+
header {
transition: padding $sidebar-transition-duration;
@@ -191,13 +202,24 @@ header {
}
}
-.tanuki-shape {
- transition: all 0.8s;
+#tanuki-logo {
- &:hover, &.highlight {
- fill: rgb(255, 255, 255);
- transition: all 0.1s;
+ #tanuki-left-ear,
+ #tanuki-right-ear,
+ #tanuki-nose {
+ @include tanuki-logo-colors($tanuki-red);
+ }
+
+ #tanuki-left-eye,
+ #tanuki-right-eye {
+ @include tanuki-logo-colors($tanuki-orange);
}
+
+ #tanuki-left-cheek,
+ #tanuki-right-cheek {
+ @include tanuki-logo-colors($tanuki-yellow);
+ }
+
}
@media (max-width: $screen-xs-max) {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 281c0a0e1e9..a0bb3427af0 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -55,17 +55,6 @@
}
}
-
-.tanuki-shape {
- transition: all 0.8s;
-
- &:hover, &.highlight {
- fill: rgb(255, 255, 255);
- transition: all 0.1s;
- }
-}
-
-
.nav-sidebar {
position: absolute;
top: 50px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index acada1f16a0..c37574ca7a1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -156,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
+/* tanuki logo colors */
+$tanuki-red: #e24329;
+$tanuki-orange: #fc6d26;
+$tanuki-yellow: #fca326;
+
/*
* State colors:
*/
@@ -263,5 +268,10 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
+/*
+ * Personal Access Tokens
+ */
+$personal-access-tokens-disabled-label-color: #bbb;
+
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss
index 28611a5ec81..9495c5b3f37 100644
--- a/app/assets/stylesheets/mailers/devise.scss
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -38,6 +38,10 @@ table {
margin: 0 auto;
text-align: left;
width: 600px;
+
+ & > td {
+ text-align: center;
+ }
}
&#body {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 22679c764dc..a34b06f1054 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -66,8 +66,7 @@
font-family: $regular_font;
}
- .gitignore-selector {
-
+ .gitignore-selector, .license-selector {
.dropdown {
line-height: 21px;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 53bff508c72..e67271adfb1 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -244,6 +244,10 @@
.panel-footer {
padding: 5px 10px;
+
+ .btn {
+ min-width: auto;
+ }
}
.commit {
@@ -252,9 +256,7 @@
}
.avatar {
- width: 20px;
- height: 20px;
- margin-right: 5px;
+ margin-left: 0;
}
.commit-row-info {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 167ab40d881..46371ec6871 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -192,6 +192,25 @@
}
}
+.personal-access-tokens-never-expires-label {
+ color: $personal-access-tokens-disabled-label-color;
+}
+
+.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
+ text-align: center;
+}
+
+.created-personal-access-token-container {
+ #created-personal-access-token {
+ width: 90%;
+ display: inline;
+ }
+
+ .btn-clipboard {
+ margin-left: 5px;
+ }
+}
+
.user-profile {
@media (max-width: $screen-xs-max) {
.cover-block {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c85d23a31f0..855d86cb238 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -28,7 +28,7 @@
.container-fluid {
position: relative;
- @media (min-width: $screen-md-max) {
+ @media (min-width: $screen-lg-min) {
.row {
display: flex;
-ms-flex-align: center;
@@ -224,7 +224,7 @@
right: 16px;
bottom: 0;
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
top: 0;
}
@@ -233,7 +233,7 @@
right: 0;
bottom: 61px;
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
position: relative;
bottom: 0;
margin-right: 10px;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 726acbdb3ed..dd1bc6f5d52 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper
include WorkhorseHelper
- before_action :authenticate_user_from_token!
+ before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
@@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base
end
end
- # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
- # https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
- def authenticate_user_from_token!
- user_token = if params[:authenticity_token].presence
- params[:authenticity_token].presence
- elsif params[:private_token].presence
- params[:private_token].presence
- elsif request.headers['PRIVATE-TOKEN'].present?
- request.headers['PRIVATE-TOKEN']
- end
- user = user_token && User.find_by_authentication_token(user_token.to_s)
+ # This filter handles both private tokens and personal access tokens
+ def authenticate_user_from_private_token!
+ token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+ user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
if user
# Notice we are passing store false, so the user is not
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
new file mode 100644
index 00000000000..508b82a9a6c
--- /dev/null
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -0,0 +1,42 @@
+class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
+ before_action :load_personal_access_tokens, only: :index
+
+ def index
+ @personal_access_token = current_user.personal_access_tokens.build
+ end
+
+ def create
+ @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
+
+ if @personal_access_token.save
+ flash[:personal_access_token] = @personal_access_token.token
+ redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
+ else
+ load_personal_access_tokens
+ render :index
+ end
+ end
+
+ def revoke
+ @personal_access_token = current_user.personal_access_tokens.find(params[:id])
+
+ if @personal_access_token.revoke!
+ flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+ else
+ flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+ end
+
+ redirect_to profile_personal_access_tokens_path
+ end
+
+ private
+
+ def personal_access_token_params
+ params.require(:personal_access_token).permit(:name, :expires_at)
+ end
+
+ def load_personal_access_tokens
+ @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
+ @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
+ end
+end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 673adca6ade..8044c637825 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -143,6 +143,7 @@ class ProjectsController < Projects::ApplicationController
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
+ labels: autocomplete.labels,
members: participants
}
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 85559fbc5f5..5b54b34070c 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -180,8 +180,8 @@ module BlobHelper
licenses = Licensee::License.all
@licenses_for_select = {
- Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
- Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
+ Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
+ Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
}
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 764d8e4e136..d618c84e983 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -341,6 +341,7 @@ module Ci
def erase_artifacts!
remove_artifacts_file!
remove_artifacts_metadata!
+ save
end
def erase(opts = {})
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 13be5b0fa5d..5b264ecffc5 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -94,10 +94,13 @@ module Ci
end
def create_builds(user, trigger_request = nil)
+ ##
+ # We persist pipeline only if there are builds available
+ #
return unless config_processor
- config_processor.stages.any? do |stage|
- CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present?
- end
+
+ build_builds_for_stages(config_processor.stages, user,
+ 'success', trigger_request) && save
end
def create_next_builds(build)
@@ -115,10 +118,10 @@ module Ci
prior_builds = latest_builds.where.not(stage: next_stages)
prior_status = prior_builds.status
- # create builds for next stages based
- next_stages.any? do |stage|
- CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present?
- end
+ # build builds for next stage that has builds available
+ # and save pipeline if we have builds
+ build_builds_for_stages(next_stages, build.user, prior_status,
+ build.trigger_request) && save
end
def retried
@@ -139,10 +142,10 @@ module Ci
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
- save_yaml_error(e.message)
+ self.yaml_errors = e.message
nil
rescue
- save_yaml_error("Undefined error")
+ self.yaml_errors = 'Undefined error'
nil
end
end
@@ -173,6 +176,17 @@ module Ci
private
+ def build_builds_for_stages(stages, user, status, trigger_request)
+ ##
+ # Note that `Array#any?` implements a short circuit evaluation, so we
+ # build builds only for the first stage that has builds available.
+ #
+ stages.any? do |stage|
+ CreateBuildsService.new(self)
+ .execute(stage, user, status, trigger_request).present?
+ end
+ end
+
def update_state
statuses.reload
self.status = if yaml_errors.blank?
@@ -185,11 +199,5 @@ module Ci
self.duration = statuses.latest.duration
save
end
-
- def save_yaml_error(error)
- return if self.yaml_errors?
- self.yaml_errors = error
- update_state
- end
end
end
diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb
deleted file mode 100644
index 5b21aac5e43..00000000000
--- a/app/models/jira_issue.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class JiraIssue < ExternalIssue
-end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
new file mode 100644
index 00000000000..c4b095e0c04
--- /dev/null
+++ b/app/models/personal_access_token.rb
@@ -0,0 +1,20 @@
+class PersonalAccessToken < ActiveRecord::Base
+ include TokenAuthenticatable
+ add_authentication_token_field :token
+
+ belongs_to :user
+
+ scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
+ scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+
+ def self.generate(params)
+ personal_access_token = self.new(params)
+ personal_access_token.ensure_token
+ personal_access_token
+ end
+
+ def revoke!
+ self.revoked = true
+ self.save
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 064b3c1fc23..ca3bc04e2dd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -262,7 +262,23 @@ class Project < ActiveRecord::Base
#
# Returns a Project, or nil if no project could be found.
def find_with_namespace(path)
- where_paths_in([path]).reorder(nil).take
+ namespace_path, project_path = path.split('/', 2)
+
+ return unless namespace_path && project_path
+
+ namespace_path = connection.quote(namespace_path)
+ project_path = connection.quote(project_path)
+
+ # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
+ # any literal matches come first, for this we have to use "BINARY".
+ # Without this there's still no guarantee in what order MySQL will return
+ # rows.
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+
+ order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
+ "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
+
+ where_paths_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple projects by their full paths.
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e5b277cb198..65d1bad511d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -243,7 +243,7 @@ class Repository
end
def cache_keys
- %i(size branch_names tag_names commit_count
+ %i(size branch_names tag_names branch_count tag_count commit_count
readme version contribution_guide changelog
license_blob license_key gitignore)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8d0427da5ab..051745fe252 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -51,6 +51,7 @@ class User < ActiveRecord::Base
# Profile
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
+ has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
@@ -267,6 +268,11 @@ class User < ActiveRecord::Base
find_by!('lower(username) = ?', username.downcase)
end
+ def find_by_personal_access_token(token_string)
+ personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
+ personal_access_token.user if personal_access_token
+ end
+
def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 3a74ae094e8..2dcb052d274 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -2,10 +2,11 @@ module Ci
class CreateBuildsService
def initialize(pipeline)
@pipeline = pipeline
+ @config = pipeline.config_processor
end
def execute(stage, user, status, trigger_request = nil)
- builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
+ builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
@@ -19,34 +20,37 @@ module Ci
end
end
+ # don't create the same build twice
+ builds_attrs.reject! do |build_attrs|
+ @pipeline.builds.find_by(ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: trigger_request,
+ name: build_attrs[:name])
+ end
+
builds_attrs.map do |build_attrs|
- # don't create the same build twice
- unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag,
- trigger_request: trigger_request, name: build_attrs[:name])
- build_attrs.slice!(:name,
- :commands,
- :tag_list,
- :options,
- :allow_failure,
- :stage,
- :stage_idx,
- :environment)
+ build_attrs.slice!(:name,
+ :commands,
+ :tag_list,
+ :options,
+ :allow_failure,
+ :stage,
+ :stage_idx,
+ :environment)
- build_attrs.merge!(ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: trigger_request,
- user: user,
- project: @pipeline.project)
+ build_attrs.merge!(pipeline: @pipeline,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: trigger_request,
+ user: user,
+ project: @pipeline.project)
- @pipeline.builds.create!(build_attrs)
- end
+ ##
+ # We do not persist new builds here.
+ # Those will be persisted when @pipeline is saved.
+ #
+ @pipeline.builds.new(build_attrs)
end
end
-
- private
-
- def config_processor
- @config_processor ||= @pipeline.config_processor
- end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index a7751b8effc..b1ee6874190 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -8,7 +8,9 @@ module Ci
return pipeline
end
- unless commit
+ if commit
+ pipeline.sha = commit.id
+ else
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
@@ -18,22 +20,18 @@ module Ci
return pipeline
end
- begin
- Ci::Pipeline.transaction do
- pipeline.sha = commit.id
+ unless pipeline.config_processor
+ pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
+ return pipeline
+ end
- unless pipeline.config_processor
- pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
- raise ActiveRecord::Rollback
- end
+ pipeline.save!
- pipeline.save!
- pipeline.create_builds(current_user)
- end
- rescue
- pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
+ unless pipeline.create_builds(current_user)
+ pipeline.errors.add(:base, 'No builds for this pipeline.')
end
+ pipeline.save
pipeline
end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 418f5cf8091..f947e8f452e 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -1,15 +1,11 @@
class CreateCommitBuildsService
def execute(project, user, params)
- return false unless project.builds_enabled?
+ return unless project.builds_enabled?
before_sha = params[:checkout_sha] || params[:before]
sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref]
- unless origin_ref && sha.present?
- return false
- end
-
ref = Gitlab::Git.ref_name(origin_ref)
tag = Gitlab::Git.tag_ref?(origin_ref)
@@ -18,23 +14,50 @@ class CreateCommitBuildsService
return false
end
- pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
+ @pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
- # Skip creating pipeline when no gitlab-ci.yml is found
- unless pipeline.ci_yaml_file
+ ##
+ # Skip creating pipeline if no gitlab-ci.yml is found
+ #
+ unless @pipeline.ci_yaml_file
return false
end
- # Create a new pipeline
- pipeline.save!
-
+ ##
# Skip creating builds for commits that have [ci skip]
- unless pipeline.skip_ci?
- # Create builds for commit
- pipeline.create_builds(user)
+ # but save pipeline object
+ #
+ if @pipeline.skip_ci?
+ return save_pipeline!
+ end
+
+ ##
+ # Skip creating builds when CI config is invalid
+ # but save pipeline object
+ #
+ unless @pipeline.config_processor
+ return save_pipeline!
end
- pipeline.touch
- pipeline
+ ##
+ # Skip creating pipeline object if there are no builds for it.
+ #
+ unless @pipeline.create_builds(user)
+ @pipeline.errors.add(:base, 'No builds created')
+ return false
+ end
+
+ save_pipeline!
+ end
+
+ private
+
+ ##
+ # Create a new pipeline and touch object to calculate status
+ #
+ def save_pipeline!
+ @pipeline.save!
+ @pipeline.touch
+ @pipeline
end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index eb73948006e..23b6668e0d1 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -11,5 +11,9 @@ module Projects
def merge_requests
@project.merge_requests.opened.select([:iid, :title])
end
+
+ def labels
+ @project.labels.select([:title, :color])
+ end
end
end
diff --git a/app/views/devise/mailer/password_change.html.haml b/app/views/devise/mailer/password_change.html.haml
new file mode 100644
index 00000000000..3349ee84807
--- /dev/null
+++ b/app/views/devise/mailer/password_change.html.haml
@@ -0,0 +1,10 @@
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ The password for your GitLab account on
+ #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}
+ has successfully been changed.
+ %p
+ If you did not initiate this change, please contact your administrator
+ immediately.
diff --git a/app/views/devise/mailer/password_change.text.erb b/app/views/devise/mailer/password_change.text.erb
new file mode 100644
index 00000000000..95923d9f8de
--- /dev/null
+++ b/app/views/devise/mailer/password_change.text.erb
@@ -0,0 +1,7 @@
+Hello, <%= @resource.name %>!
+
+The password for your GitLab account on <%= Gitlab.config.gitlab.url %>
+has successfully been changed.
+
+If you did not initiate this change, please contact your administrator
+immediately.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
deleted file mode 100644
index 23b31da92d8..00000000000
--- a/app/views/devise/mailer/reset_password_instructions.html.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-<p>Hello <%= @resource.email %>!</p>
-
-<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
-
-<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
-
-<p>If you didn't request this, please ignore this email.</p>
-<p>Your password won't change until you access the link above and create a new one.</p>
diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml
new file mode 100644
index 00000000000..e91c9522520
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.html.haml
@@ -0,0 +1,12 @@
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ Someone, hopefully you, has requested to reset the password for your
+ GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}.
+ %p
+ If you did not perform this request, you can safely ignore this email.
+ %p
+ Otherwise, click the link below to complete the process.
+ #cta
+ = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token))
diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb
new file mode 100644
index 00000000000..116313ee11c
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.text.erb
@@ -0,0 +1,10 @@
+Hello, <%= @resource.name %>!
+
+Someone, hopefully you, has requested to reset the password for your GitLab
+account on <%= Gitlab.config.gitlab.url %>
+
+If you did not perform this request, you can safely ignore this email.
+
+Otherwise, click the link below to complete the process:
+
+<%= edit_password_url(@resource, reset_password_token: @token) %>
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
index 52b327e20c5..9990d1ccac6 100644
--- a/app/views/devise/mailer/unlock_instructions.html.haml
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -1,10 +1,9 @@
-%p
-Hello #{@resource.name}!
-
-%p
- Your GitLab account has been locked due to an excessive amount of unsuccessful
- sign in attempts. Your account will automatically unlock in
- = time_ago_in_words(Devise.unlock_in.from_now)
- or you may click the link below to unlock now.
-
-%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token)
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ Your GitLab account has been locked due to an excessive amount of unsuccessful
+ sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
+ or you may click the link below to unlock now.
+ #cta
+ = link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
new file mode 100644
index 00000000000..3aea3e20145
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -0,0 +1,7 @@
+Hello, <%= @resource.name %>!
+
+Your GitLab account has been locked due to an excessive amount of unsuccessful
+sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
+or you may click the link below to unlock now.
+
+<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index d4b1f477f3f..bb6f14a6225 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -13,6 +13,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
+ = nav_link(controller: :personal_access_tokens) do
+ = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
+ %span
+ Personal Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
new file mode 100644
index 00000000000..1b45548bd02
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -0,0 +1,105 @@
+- page_title "Personal Access Tokens"
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can generate a personal access token for each application you use that needs access to the GitLab API.
+ .col-lg-9
+
+ - if flash[:personal_access_token]
+ .created-personal-access-token-container
+ %h5.prepend-top-0
+ Your New Personal Access Token
+ .form-group
+ = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
+ = clipboard_button(clipboard_text: flash[:personal_access_token])
+ %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
+
+ %hr
+
+ %h5.prepend-top-0
+ Add a Personal Access Token
+ %p.profile-settings-content
+ Pick a name for the application, and we'll give you a unique token.
+ = form_for [:profile, @personal_access_token],
+ method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(@personal_access_token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control", required: false
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
+
+ %hr
+
+ %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
+
+ - if @active_personal_access_tokens.present?
+ .table-responsive
+ %table.table.active-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %th Expires
+ %th
+ %tbody
+ - @active_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires_at.present?
+ = token.expires_at.to_date.to_s(:medium)
+ - else
+ %span.personal-access-tokens-never-expires-label Never
+ %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ You don't have any active tokens yet.
+
+ %hr
+
+ %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
+
+ - if @inactive_personal_access_tokens.present?
+ .table-responsive
+ %table.table.inactive-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %tbody
+ - @inactive_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+
+ - else
+ .settings-message.text-center
+ There are no inactive tokens.
+
+
+:javascript
+ var date = $('#personal_access_token_expires_at').val();
+
+ var datepicker = $(".datepicker").datepicker({
+ dateFormat: "yy-mm-dd",
+ minDate: 0
+ });
+
+ $("#created-personal-access-token").click(function() {
+ this.select();
+ });
+
+ $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 7c2b8d01508..e0ca2a3109c 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,15 +1,15 @@
- if event = last_push_event
- if show_last_push_widget?(event)
-
.row-content-block.top-block.clear-block.hidden-xs
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
- %strong= event.ref_name
- branch
- #{time_ago_with_tooltip(event.created_at)}
+ %div{ class: (container_class) }
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ %strong= event.ref_name
+ branch
+ #{time_ago_with_tooltip(event.created_at)}
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4071b59c003..ae89637df60 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -13,12 +13,10 @@
required: true, class: 'form-control new-file-name'
.pull-right
- .license-selector.js-license-selector.hide
- = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
-
- .gitignore-selector.hidden
- = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } )
-
+ .license-selector.js-license-selector-wrap.hidden
+ = dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
+ .gitignore-selector.js-gitignore-selector-wrap.hidden
+ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index a26f8aeb315..4e2702c2e44 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -48,16 +48,16 @@
- if @build.active?
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- #js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
+ #js-build-scroll.scroll-controls
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index b08524574e4..de39964fca8 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+ = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index c4df8bd504f..2ec96308fd7 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -17,11 +17,11 @@
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
- %span.dropdown
+ %span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
%span.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
@@ -37,7 +37,7 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .light.prepend-top-default
+ .light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4afa902b4eb..e9ca46a74bf 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -23,10 +23,10 @@
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
- #{'Branch'.pluralize(@repository.branch_names.count)} (#{number_with_delimiter(@repository.branch_names.count)})
+ #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
= link_to namespace_project_tags_path(@project.namespace, @project) do
- #{'Tag'.pluralize(@repository.tag_names.count)} (#{number_with_delimiter(@repository.tag_names.count)})
+ #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme
%li
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 2ddc5d504fa..a3a4dba3fa4 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -1,8 +1,9 @@
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- %span.str-truncated
- = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
+ - file_name = blob_item.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
+ %span.str-truncated= file_name
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index cf65057e704..9577696fc0d 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -1,9 +1,9 @@
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
- %span.str-truncated
- - path = flatten_tree(tree_item)
- = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
+ - path = flatten_tree(tree_item)
+ = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
+ %span.str-truncated= path
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb
index 8fd27b1d88e..de2cdc6ecae 100644
--- a/config/initializers/default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -9,3 +9,4 @@ unless Gitlab.config.gitlab_on_standard_port?
end
Rails.application.routes.default_url_options = default_url_options
+ActionMailer::Base.asset_host = Settings.gitlab['base_url']
diff --git a/config/routes.rb b/config/routes.rb
index 09bd9ac55a9..87da5e7178f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -352,6 +352,13 @@ Rails.application.routes.draw do
resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
+
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
diff --git a/db/migrate/20160415062917_create_personal_access_tokens.rb b/db/migrate/20160415062917_create_personal_access_tokens.rb
new file mode 100644
index 00000000000..ce0b33f32bd
--- /dev/null
+++ b/db/migrate/20160415062917_create_personal_access_tokens.rb
@@ -0,0 +1,13 @@
+class CreatePersonalAccessTokens < ActiveRecord::Migration
+ def change
+ create_table :personal_access_tokens do |t|
+ t.references :user, index: true, foreign_key: true, null: false
+ t.string :token, index: { unique: true }, null: false
+ t.string :name, null: false
+ t.boolean :revoked, default: false
+ t.datetime :expires_at
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d6a542a89fd..5a27e9d5cdc 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -756,6 +756,19 @@ ActiveRecord::Schema.define(version: 20160616084004) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "personal_access_tokens", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "token", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "revoked", default: false
+ t.datetime "expires_at"
+ end
+
+ add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
@@ -1095,5 +1108,6 @@ ActiveRecord::Schema.define(version: 20160616084004) do
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_foreign_key "personal_access_tokens", "users"
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/api/README.md b/doc/api/README.md
index e3fc5a09f21..71bb01e0d51 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -44,13 +44,11 @@ The following documentation is for the [internal CI API](ci/README.md):
## Authentication
-All API requests require authentication. You need to pass a `private_token`
-parameter via query string or header. If passed as a header, the header name
-must be `PRIVATE-TOKEN` (uppercase and with a dash instead of an underscore).
-You can find or reset your private token in your account page (`/profile/account`).
+All API requests require authentication via a token. There are three types of tokens
+available: private tokens, OAuth 2 tokens, and personal access tokens.
-If `private_token` is invalid or omitted, then an error message will be
-returned with status code `401`:
+If a token is invalid or omitted, an error message will be returned with
+status code `401`:
```json
{
@@ -58,42 +56,56 @@ returned with status code `401`:
}
```
-API requests should be prefixed with `api` and the API version. The API version
-is defined in [`lib/api.rb`][lib-api-url].
+### Private Tokens
-Example of a valid API request:
+You need to pass a `private_token` parameter via query string or header. If passed as a
+header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
+an underscore). You can find or reset your private token in your account page
+(`/profile/account`).
-```shell
-GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
-```
+### OAuth 2 Tokens
-Example of a valid API request using cURL and authentication via header:
+You can use an OAuth 2 token to authenticate with the API by passing it either in the
+`access_token` parameter or in the `Authorization` header.
+
+Example of using the OAuth2 token in the header:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
```
-The API uses JSON to serialize data. You don't need to specify `.json` at the
-end of an API URL.
+Read more about [GitLab as an OAuth2 client](oauth2.md).
+
+### Personal Access Tokens
-## Authentication with OAuth2 token
+> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8
-Instead of the `private_token` you can transmit the OAuth2 access token as a
-header or as a parameter.
+You can create as many personal access tokens as you like from your GitLab
+profile (`/profile/personal_access_tokens`); perhaps one for each application
+that needs access to the GitLab API.
-Example of OAuth2 token as a parameter:
+Once you have your token, pass it to the API using either the `private_token`
+parameter or the `PRIVATE-TOKEN` header.
+
+## Basic Usage
+
+API requests should be prefixed with `api` and the API version. The API version
+is defined in [`lib/api.rb`][lib-api-url].
+
+Example of a valid API request:
```shell
-curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
```
-Example of OAuth2 token as a header:
+Example of a valid API request using cURL and authentication via header:
```shell
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+The API uses JSON to serialize data. You don't need to specify `.json` at the
+end of an API URL.
## Status codes
@@ -330,3 +342,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API"
[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
+[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index de5959e3aae..77e407b54c5 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -9,9 +9,13 @@ module API
[ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
end
+ def find_user_by_private_token
+ token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+ User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+ end
+
def current_user
- private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
- @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
+ @current_user ||= (find_user_by_private_token || doorkeeper_guard)
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
return nil
@@ -33,7 +37,7 @@ module API
identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
# Regex for integers
- if !!(identifier =~ /^[0-9]+$/)
+ if !!(identifier =~ /\A[0-9]+\z/)
identifier.to_i
else
identifier
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index db95d7c908b..4815bafe238 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -103,7 +103,7 @@ module Banzai
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
- each_node do |node|
+ nodes.each do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
@@ -206,6 +206,55 @@ module Banzai
text
end
+ # Returns a Hash containing all object references (e.g. issue IDs) per the
+ # project they belong to.
+ def references_per_project
+ @references_per_project ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+
+ regex = Regexp.union(object_class.reference_pattern,
+ object_class.link_reference_pattern)
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ project = $~[:project] || current_project_path
+
+ refs[project] << $~[object_sym]
+ end
+ end
+
+ refs
+ end
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def projects_per_reference
+ @projects_per_reference ||= begin
+ hash = {}
+ refs = Set.new
+
+ references_per_project.each do |project_ref, _|
+ refs << project_ref
+ end
+
+ find_projects_for_paths(refs.to_a).each do |project|
+ hash[project.path_with_namespace] = project
+ end
+
+ hash
+ end
+ end
+
+ # Returns the projects for the given paths.
+ def find_projects_for_paths(paths)
+ Project.where_paths_in(paths).includes(:namespace)
+ end
+
+ def current_project_path
+ @current_project_path ||= project.path_with_namespace
+ end
+
private
def project_refs_cache
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index f73ecfc9418..0a29c547a4d 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -3,17 +3,8 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
- doc.search('a').each do |node|
- link = node.attr('href')
-
- next unless link
-
- # Skip non-HTTP(S) links
- next unless link.start_with?('http')
-
- # Skip internal links
- next if link.start_with?(internal_url)
-
+ # Skip non-HTTP(S) links and internal links
+ doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 2496e704002..2614261f9eb 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -11,13 +11,40 @@ module Banzai
Issue
end
- def find_object(project, id)
- project.get_issue(id)
+ def find_object(project, iid)
+ issues_per_project[project][iid]
end
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
end
+
+ def project_from_ref(ref)
+ projects_per_reference[ref || current_project_path]
+ end
+
+ # Returns a Hash containing the issues per Project instance.
+ def issues_per_project
+ @issues_per_project ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ projects_per_reference.each do |path, project|
+ issue_ids = references_per_project[path]
+
+ next unless project.default_issues_tracker?
+
+ project.issues.where(iid: issue_ids.to_a).each do |issue|
+ hash[project][issue.iid] = issue
+ end
+ end
+
+ hash
+ end
+ end
+
+ def find_projects_for_paths(paths)
+ super(paths).includes(:gitlab_issue_tracker_service)
+ end
end
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 68246497e90..ed86de819eb 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -30,7 +30,10 @@ module Ci
end
def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
- builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
+ builds.select do |build|
+ build[:stage] == stage &&
+ process?(build[:only], build[:except], ref, tag, trigger_request)
+ end
end
def builds
@@ -51,7 +54,7 @@ module Ci
job = @jobs[name.to_sym]
return [] unless job
- job.fetch(:variables, [])
+ job[:variables] || []
end
private
@@ -201,12 +204,12 @@ module Ci
raise ValidationError, "#{name} job: tags parameter should be an array of strings"
end
- if job[:only] && !validate_array_of_strings(job[:only])
- raise ValidationError, "#{name} job: only parameter should be an array of strings"
+ if job[:only] && !validate_array_of_strings_or_regexps(job[:only])
+ raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps"
end
- if job[:except] && !validate_array_of_strings(job[:except])
- raise ValidationError, "#{name} job: except parameter should be an array of strings"
+ if job[:except] && !validate_array_of_strings_or_regexps(job[:except])
+ raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps"
end
if job[:allow_failure] && !validate_boolean(job[:allow_failure])
diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/validation_helpers.rb
index 3900fc89391..72f648975dc 100644
--- a/lib/gitlab/ci/config/node/validation_helpers.rb
+++ b/lib/gitlab/ci/config/node/validation_helpers.rb
@@ -15,6 +15,10 @@ module Gitlab
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
+ end
+
def validate_variables(variables)
variables.is_a?(Hash) &&
variables.all? { |key, value| validate_string(key) && validate_string(value) }
@@ -24,6 +28,19 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_string_or_regexp(value)
+ return true if value.is_a?(Symbol)
+ return false unless value.is_a?(String)
+
+ if value.first == '/' && value.last == '/'
+ Regexp.new(value[1...-1])
+ else
+ true
+ end
+ rescue RegexpError
+ false
+ end
+
def validate_environment(value)
value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 186239d3096..ff5b3916273 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -30,4 +30,75 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
end
+
+ describe "#authenticate_user_from_token!" do
+ describe "authenticating a user from a private token" do
+ controller(ApplicationController) do
+ def index
+ render text: "authenticated"
+ end
+ end
+
+ let(:user) { create(:user) }
+
+ context "when the 'private_token' param is populated with the private token" do
+ it "logs the user in" do
+ get :index, private_token: user.private_token
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+
+ context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
+ it "logs the user in" do
+ @request.headers['PRIVATE-TOKEN'] = user.private_token
+ get :index
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ @request.headers['PRIVATE-TOKEN'] = "token"
+ get :index, private_token: "token", authenticity_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq("authenticated")
+ end
+ end
+
+ describe "authenticating a user from a personal access token" do
+ controller(ApplicationController) do
+ def index
+ render text: 'authenticated'
+ end
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
+ end
+ end
+ end
end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
new file mode 100644
index 00000000000..da4c72bcb5b
--- /dev/null
+++ b/spec/factories/personal_access_tokens.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :personal_access_token do
+ user
+ token { SecureRandom.hex(50) }
+ name { FFaker::Product.brand }
+ revoked false
+ expires_at { 5.days.from_now }
+ end
+end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..a85930c7543
--- /dev/null
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'Profile > Personal Access Tokens', feature: true, js: true do
+ let(:user) { create(:user) }
+
+ def active_personal_access_tokens
+ find(".table.active-personal-access-tokens")
+ end
+
+ def inactive_personal_access_tokens
+ find(".table.inactive-personal-access-tokens")
+ end
+
+ def created_personal_access_token
+ find("#created-personal-access-token").value
+ end
+
+ def disallow_personal_access_token_saves!
+ allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false)
+ errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
+ allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
+ end
+
+ before do
+ login_as(user)
+ end
+
+ describe "token creation" do
+ it "allows creation of a token" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text("Never")
+ end
+
+ it "allows creation of a token with an expiry date" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ # Set date to 1st of next month
+ find_field("Expires at").trigger('focus')
+ find("a[title='Next']").click
+ click_on "1"
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ end
+
+ context "when creation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count }
+ expect(page).to have_content("Name cannot be nil")
+ end
+ end
+ end
+
+ describe "inactive tokens" do
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "allows revocation of an active token" do
+ visit profile_personal_access_tokens_path
+ click_on "Revoke"
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ it "moves expired tokens to the 'inactive' section" do
+ personal_access_token.update(expires_at: 5.days.ago)
+ visit profile_personal_access_tokens_path
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ context "when revocation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+
+ expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+ expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(page).to have_content("Could not revoke")
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index ecc818eb1e1..e1e105e6bbe 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'project owner creates a license file', feature: true, js: true do
- include Select2Helper
+ include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:project) }
@@ -21,7 +21,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
@@ -44,7 +44,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
@@ -58,4 +58,12 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_content('The MIT License (MIT)')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
+
+ def select_template(template)
+ page.within('.js-license-selector-wrap') do
+ click_button 'Choose a License template'
+ click_link template
+ wait_for_ajax
+ end
+ end
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 34eda29c285..67aac25e427 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
- include Select2Helper
+ include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:empty_project) }
@@ -20,7 +20,7 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
@@ -36,4 +36,12 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(page).to have_content('The MIT License (MIT)')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
+
+ def select_template(template)
+ page.within('.js-license-selector-wrap') do
+ click_button 'Choose a License template'
+ click_link template
+ wait_for_ajax
+ end
+ end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 8550d279d09..6a39c302f55 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -77,6 +77,7 @@ feature 'Prioritize labels', feature: true do
end
visit current_url
+ wait_for_ajax
page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix')
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 639b28d49ee..1bd354815e4 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -49,6 +49,13 @@ describe NotesFinder do
user = create(:user)
expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
end
+
+ it 'raises an error for project members with guest role' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
end
end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index a3336c87173..903224589dd 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -33,9 +33,9 @@ describe MergeRequestsHelper do
let(:project) { create(:project) }
let(:issues) do
[
- JiraIssue.new('JIRA-123', project),
- JiraIssue.new('JIRA-456', project),
- JiraIssue.new('FOOBAR-7890', project)
+ ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('JIRA-456', project),
+ ExternalIssue.new('FOOBAR-7890', project)
]
end
diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee
index 22ebc7039d1..3cb67d51c85 100644
--- a/spec/javascripts/merge_request_spec.js.coffee
+++ b/spec/javascripts/merge_request_spec.js.coffee
@@ -6,7 +6,7 @@ describe 'MergeRequest', ->
beforeEach ->
fixture.load('merge_requests_show.html')
- @merge = new MergeRequest({})
+ @merge = new MergeRequest()
it 'modifies the Markdown field', ->
spyOn(jQuery, 'ajax').and.stub()
diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
new file mode 100644
index 00000000000..0c55d8e19da
--- /dev/null
+++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AbstractReferenceFilter do
+ let(:project) { create(:empty_project) }
+
+ describe '#references_per_project' do
+ it 'returns a Hash containing references grouped per project paths' do
+ doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2")
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:object_class).twice.and_return(Issue)
+ expect(filter).to receive(:object_sym).twice.and_return(:issue)
+
+ refs = filter.references_per_project
+
+ expect(refs).to be_an_instance_of(Hash)
+ expect(refs[project.to_reference]).to eq(Set.new(%w[1 2]))
+ end
+ end
+
+ describe '#projects_per_reference' do
+ it 'returns a Hash containing projects grouped per project paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:references_per_project).
+ and_return({ project.path_with_namespace => Set.new(%w[1]) })
+
+ expect(filter.projects_per_reference).
+ to eq({ project.path_with_namespace => project })
+ end
+ end
+
+ describe '#find_projects_for_paths' do
+ it 'returns a list of Projects for a list of paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.find_projects_for_paths([project.path_with_namespace])).
+ to eq([project])
+ end
+ end
+
+ describe '#current_project_path' do
+ it 'returns the path of the current project' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.current_project_path).to eq(project.path_with_namespace)
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index f4c5c621bd0..695a5bc6fd4 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -19,19 +19,31 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
- it 'adds rel="nofollow" to external links' do
- act = %q(<a href="https://google.com/">Google</a>)
- doc = filter(act)
-
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ context 'for root links on document' do
+ let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
end
- it 'adds rel="noreferrer" to external links' do
- act = %q(<a href="https://google.com/">Google</a>)
- doc = filter(act)
+ context 'for nested links on document' do
+ let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 8e6a264970d..25f0bc2092f 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -25,7 +25,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
- expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project, issue.iid).
+ and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
@@ -107,8 +109,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference(project) }
it 'ignores valid references when cross-reference project uses external tracker' do
- expect_any_instance_of(Project).to receive(:get_issue).
- with(issue.iid).and_return(nil)
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project2, issue.iid).
+ and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 143e2e6d238..d562d8b25ea 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -157,6 +157,35 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ shared_examples 'raises an error' do
+ it do
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is integer' do
+ let(:only) { 1 }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
+
+ it_behaves_like 'raises an error'
+ end
+ end
end
describe :except do
@@ -284,16 +313,44 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
end
- end
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ shared_examples 'raises an error' do
+ it do
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is integer' do
+ let(:except) { 1 }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
+
+ it_behaves_like 'raises an error'
+ end
+ end
+ end
end
-
+
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
-
+
subject { config_processor.builds_for_stage_and_ref("test", "master").first }
-
+
describe "before_script" do
context "in global context" do
let(:config) do
@@ -302,12 +359,12 @@ module Ci
test: { script: ["script"] }
}
end
-
+
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("global script\nscript")
end
end
-
+
context "overwritten in local context" do
let(:config) do
{
@@ -465,19 +522,41 @@ module Ci
end
context 'when syntax is incorrect' do
- it 'raises error' do
- variables = [:KEY1, 'value1', :KEY2, 'value2']
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ context 'when variables defined but invalid' do
+ it 'raises error' do
+ variables = [:KEY1, 'value1', :KEY2, 'value2']
+
+ config = YAML.dump(
+ { before_script: ['pwd'],
+ rspec: {
+ variables: variables,
+ script: 'rspec' }
+ })
+
+ expect { GitlabCiYamlProcessor.new(config, path) }
+ .to raise_error(GitlabCiYamlProcessor::ValidationError,
+ /job: variables should be a map/)
+ end
+ end
- expect { GitlabCiYamlProcessor.new(config, path) }
- .to raise_error(GitlabCiYamlProcessor::ValidationError,
- /job: variables should be a map/)
+ context 'when variables key defined but value not specified' do
+ it 'returns empty array' do
+ config = YAML.dump(
+ { before_script: ['pwd'],
+ rspec: {
+ variables: nil,
+ script: 'rspec' }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ ##
+ # TODO, in next version of CI configuration processor this
+ # should be invalid configuration, see #18775 and #15060
+ #
+ expect(config_processor.job_variables(:rspec))
+ .to be_an_instance_of(Array).and be_empty
+ end
end
end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7c617723e6d..7b4ccc83915 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -105,7 +105,8 @@ describe Gitlab::ReferenceExtractor, lib: true do
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
- expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
+ expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('FOOBAR-4567', project)]
end
end
diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb
index dc3062a4332..d6588efc486 100644
--- a/spec/mailers/previews/devise_mailer_preview.rb
+++ b/spec/mailers/previews/devise_mailer_preview.rb
@@ -1,11 +1,30 @@
class DeviseMailerPreview < ActionMailer::Preview
def confirmation_instructions_for_signup
- user = User.new(name: 'Jane Doe', email: 'signup@example.com')
- DeviseMailer.confirmation_instructions(user, 'faketoken', {})
+ DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
end
def confirmation_instructions_for_new_email
user = User.last
+ user.unconfirmed_email = 'unconfirmed@example.com'
+
DeviseMailer.confirmation_instructions(user, 'faketoken', {})
end
+
+ def reset_password_instructions
+ DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def unlock_instructions
+ DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def password_change
+ DeviseMailer.password_change(unsaved_user, {})
+ end
+
+ private
+
+ def unsaved_user
+ User.new(name: 'Jane Doe', email: 'jdoe@example.com')
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 0d769ed7324..34507cf5083 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -258,6 +258,19 @@ describe Ci::Pipeline, models: true do
end
end
end
+
+ context 'when no builds created' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
+ end
+
+ it 'returns false' do
+ expect(pipeline.create_builds(nil)).to be_falsey
+ expect(pipeline).not_to be_persisted
+ end
+ end
end
describe "#finished_at" do
diff --git a/spec/models/jira_issue_spec.rb b/spec/models/jira_issue_spec.rb
deleted file mode 100644
index 1634265b439..00000000000
--- a/spec/models/jira_issue_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'spec_helper'
-
-describe JiraIssue do
- let(:project) { create(:project) }
- subject { JiraIssue.new('JIRA-123', project) }
-
- describe 'id' do
- subject { super().id }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe 'iid' do
- subject { super().iid }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe 'to_s' do
- subject { super().to_s }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe :== do
- specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) }
- specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) }
-
- it 'only compares with JiraIssues' do
- expect(subject).not_to eq('JIRA-123')
- end
- end
-end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
new file mode 100644
index 00000000000..46eb71cef14
--- /dev/null
+++ b/spec/models/personal_access_token_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe PersonalAccessToken, models: true do
+ describe ".generate" do
+ it "generates a random token" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token.token).to be_present
+ end
+
+ it "doesn't save the record" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5309cfb99ff..c9517324541 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -76,7 +76,8 @@ describe JiraService, models: true do
end
it "should call JIRA API" do
- @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request,
+ ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
@@ -84,7 +85,8 @@ describe JiraService, models: true do
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
- @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request,
+ ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @api_url).with(
body: /this-is-a-custom-id/
).once
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index fedab1f913b..53c8408633c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -220,7 +220,7 @@ describe Project, models: true do
end
end
- describe :find_with_namespace do
+ describe '.find_with_namespace' do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
@@ -231,6 +231,22 @@ describe Project, models: true do
it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
end
+
+ context 'when multiple projects using a similar name exist' do
+ let(:group) { create(:group, name: 'gitlab') }
+
+ let!(:project1) do
+ create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
+ end
+
+ let!(:project2) do
+ create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
+ end
+
+ it 'returns the row where the path matches literally' do
+ expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
+ end
+ end
end
describe :to_param do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 0c19094ec54..f22db61e744 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
-describe API, api: true do
+describe API::Helpers, api: true do
+
include API::Helpers
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -39,24 +41,64 @@ describe API, api: true do
end
describe ".current_user" do
- it "should return nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it "should return nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
+ describe "when authenticating using a user's private token" do
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ expect(current_user).to eq(user)
+ end
end
- it "should leave user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- expect(current_user).to eq(user)
- clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
- expect(current_user).to eq(user)
+ describe "when authenticating using a user's personal access tokens" do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+ expect(current_user).to eq(user)
+ end
+
+ it 'does not allow revoked tokens' do
+ personal_access_token.revoke!
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it 'does not allow expired tokens' do
+ personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
end
it "should change current user to sudo when admin" do
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
index 984b78487d4..8b0becd83d3 100644
--- a/spec/services/ci/create_builds_service_spec.rb
+++ b/spec/services/ci/create_builds_service_spec.rb
@@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do
#
subject do
- described_class.new(pipeline).execute('test', nil, user, status)
+ described_class.new(pipeline).execute('test', user, status, nil)
end
context 'next builds available' do
@@ -17,6 +17,10 @@ describe Ci::CreateBuildsService, services: true do
it { is_expected.to be_an_instance_of Array }
it { is_expected.to all(be_an_instance_of Ci::Build) }
+
+ it 'does not persist created builds' do
+ expect(subject.first).not_to be_persisted
+ end
end
context 'builds skipped' do
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
index a5b4d9f05de..deab242f45a 100644
--- a/spec/services/create_commit_builds_service_spec.rb
+++ b/spec/services/create_commit_builds_service_spec.rb
@@ -39,7 +39,7 @@ describe CreateCommitBuildsService, services: true do
end
it "creates commit if there is no appropriate job but deploy job has right ref setting" do
- config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
+ config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
stub_ci_pipeline_yaml_file(config)
result = service.execute(project, user,
@@ -81,7 +81,7 @@ describe CreateCommitBuildsService, services: true do
expect(pipeline.yaml_errors).not_to be_nil
end
- describe :ci_skip? do
+ context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
before do
@@ -171,5 +171,24 @@ describe CreateCommitBuildsService, services: true do
expect(pipeline.status).to eq("failed")
expect(pipeline.builds.any?).to be false
end
+
+ context 'when there are no jobs for this pipeline' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_falsey
+ expect(Ci::Build.all).to be_empty
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 18692f1279a..f99ad046f0d 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -312,7 +312,8 @@ describe GitPushService, services: true do
end
it "doesn't close issues when external issue tracker is in use" do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ allow_any_instance_of(Project).to receive(:default_issues_tracker?).
+ and_return(false)
# The push still shouldn't create cross-reference notes.
expect do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 09f0ee3871d..85dd30bf48c 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -529,7 +529,7 @@ describe SystemNoteService, services: true do
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
- let(:jira_issue) { JiraIssue.new("JIRA-1", project)}
+ let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
let(:commit) { project.commit }
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index e3827cae9a6..7d6668920c0 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -20,6 +20,10 @@ describe ExpireBuildArtifactsWorker do
it 'does remove files' do
expect(build.reload.artifacts_file.exists?).to be_falsey
end
+
+ it 'does nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).to be_nil
+ end
end
context 'with not yet expired artifacts' do
@@ -32,6 +36,10 @@ describe ExpireBuildArtifactsWorker do
it 'does not remove files' do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
end
context 'without expire date' do
@@ -44,6 +52,10 @@ describe ExpireBuildArtifactsWorker do
it 'does not remove files' do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
end
context 'for expired artifacts' do