mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-14 15:16:04 +10:00
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 7.13.0 (unreleased)
|
||||
- Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu)
|
||||
- Support commenting on diffs in side-by-side mode (Stan Hu)
|
||||
- Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu)
|
||||
- Remove project visibility icons from dashboard projects list
|
||||
@@ -9,6 +10,8 @@ v 7.13.0 (unreleased)
|
||||
- Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8
|
||||
|
||||
v 7.12.0 (unreleased)
|
||||
- Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu)
|
||||
- Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu)
|
||||
- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu)
|
||||
- Update oauth button logos for Twitter and Google to recommended assets
|
||||
- Fix hooks for web based events with external issue references (Daniel Gerhardt)
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
class @BlobView
|
||||
constructor: ->
|
||||
# handle multi-line select
|
||||
handleMultiSelect = (e) ->
|
||||
[ first_line, last_line ] = parseSelectedLines()
|
||||
[ line_number ] = parseSelectedLines($(this).attr("id"))
|
||||
hash = "L#{line_number}"
|
||||
|
||||
if e.shiftKey and not isNaN(first_line) and not isNaN(line_number)
|
||||
if line_number < first_line
|
||||
last_line = first_line
|
||||
first_line = line_number
|
||||
else
|
||||
last_line = line_number
|
||||
|
||||
hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}"
|
||||
|
||||
setHash(hash)
|
||||
e.preventDefault()
|
||||
|
||||
# See if there are lines selected
|
||||
# "#L12" and "#L34-56" supported
|
||||
highlightBlobLines = (e) ->
|
||||
[ first_line, last_line ] = parseSelectedLines()
|
||||
|
||||
unless isNaN first_line
|
||||
$("#tree-content-holder .highlight .line").removeClass("hll")
|
||||
$("#LC#{line}").addClass("hll") for line in [first_line..last_line]
|
||||
$.scrollTo("#L#{first_line}", offset: -50) unless e?
|
||||
|
||||
# parse selected lines from hash
|
||||
# always return first and last line (initialized to NaN)
|
||||
parseSelectedLines = (str) ->
|
||||
first_line = NaN
|
||||
last_line = NaN
|
||||
hash = str || window.location.hash
|
||||
|
||||
if hash isnt ""
|
||||
matches = hash.match(/\#?L(\d+)(\-(\d+))?/)
|
||||
first_line = parseInt(matches?[1])
|
||||
last_line = parseInt(matches?[3])
|
||||
last_line = first_line if isNaN(last_line)
|
||||
|
||||
[ first_line, last_line ]
|
||||
|
||||
setHash = (hash) ->
|
||||
hash = hash.replace(/^\#/, "")
|
||||
nodes = $("#" + hash)
|
||||
# if any nodes are using this id, they must be temporarily changed
|
||||
# also, add a temporary div at the top of the screen to prevent scrolling
|
||||
if nodes.length > 0
|
||||
scroll_top = $(document).scrollTop()
|
||||
nodes.attr("id", "")
|
||||
tmp = $("<div></div>")
|
||||
.css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" })
|
||||
.attr("id", hash)
|
||||
.appendTo(document.body)
|
||||
|
||||
window.location.hash = hash
|
||||
|
||||
# restore the nodes
|
||||
if nodes.length > 0
|
||||
tmp.remove()
|
||||
nodes.attr("id", hash)
|
||||
|
||||
# initialize multi-line select
|
||||
$("#tree-content-holder .line-numbers a[id^=L]").on("click", handleMultiSelect)
|
||||
|
||||
# Highlight the correct lines on load
|
||||
highlightBlobLines()
|
||||
|
||||
# Highlight the correct lines when the hash part of the URL changes
|
||||
$(window).on("hashchange", highlightBlobLines)
|
||||
@@ -87,7 +87,7 @@ class Dispatcher
|
||||
new TreeView()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:blob:show'
|
||||
new BlobView()
|
||||
new LineHighlighter()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:labels:new', 'projects:labels:edit'
|
||||
new Labels()
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# LineHighlighter
|
||||
#
|
||||
# Handles single- and multi-line selection and highlight for blob views.
|
||||
#
|
||||
#= require jquery.scrollTo
|
||||
#
|
||||
# ### Example Markup
|
||||
#
|
||||
# <div id="tree-content-holder">
|
||||
# <div class="file-content">
|
||||
# <div class="line-numbers">
|
||||
# <a href="#L1" id="L1" data-line-number="1">1</a>
|
||||
# <a href="#L2" id="L2" data-line-number="2">2</a>
|
||||
# <a href="#L3" id="L3" data-line-number="3">3</a>
|
||||
# <a href="#L4" id="L4" data-line-number="4">4</a>
|
||||
# <a href="#L5" id="L5" data-line-number="5">5</a>
|
||||
# </div>
|
||||
# <pre class="code highlight">
|
||||
# <code>
|
||||
# <span id="LC1" class="line">...</span>
|
||||
# <span id="LC2" class="line">...</span>
|
||||
# <span id="LC3" class="line">...</span>
|
||||
# <span id="LC4" class="line">...</span>
|
||||
# <span id="LC5" class="line">...</span>
|
||||
# </code>
|
||||
# </pre>
|
||||
# </div>
|
||||
# </div>
|
||||
#
|
||||
class @LineHighlighter
|
||||
# CSS class applied to highlighted lines
|
||||
highlightClass: 'hll'
|
||||
|
||||
# Internal copy of location.hash so we're not dependent on `location` in tests
|
||||
_hash: ''
|
||||
|
||||
# Initialize a LineHighlighter object
|
||||
#
|
||||
# hash - String URL hash for dependency injection in tests
|
||||
constructor: (hash = location.hash) ->
|
||||
@_hash = hash
|
||||
|
||||
@bindEvents()
|
||||
|
||||
unless hash == ''
|
||||
range = @hashToRange(hash)
|
||||
|
||||
if range[0]
|
||||
@highlightRange(range)
|
||||
|
||||
# Scroll to the first highlighted line on initial load
|
||||
# Offset -50 for the sticky top bar, and another -100 for some context
|
||||
$.scrollTo("#L#{range[0]}", offset: -150)
|
||||
|
||||
bindEvents: ->
|
||||
$('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler
|
||||
|
||||
# While it may seem odd to bind to the mousedown event and then throw away
|
||||
# the click event, there is a method to our madness.
|
||||
#
|
||||
# If not done this way, the line number anchor will sometimes keep its
|
||||
# active state even when the event is cancelled, resulting in an ugly border
|
||||
# around the link and/or a persisted underline text decoration.
|
||||
|
||||
$('#tree-content-holder').on 'click', 'a[data-line-number]', (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
clickHandler: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
@clearHighlight()
|
||||
|
||||
lineNumber = $(event.target).data('line-number')
|
||||
current = @hashToRange(@_hash)
|
||||
|
||||
unless current[0] && event.shiftKey
|
||||
# If there's no current selection, or there is but Shift wasn't held,
|
||||
# treat this like a single-line selection.
|
||||
@setHash(lineNumber)
|
||||
@highlightLine(lineNumber)
|
||||
else if event.shiftKey
|
||||
if lineNumber < current[0]
|
||||
range = [lineNumber, current[0]]
|
||||
else
|
||||
range = [current[0], lineNumber]
|
||||
|
||||
@setHash(range[0], range[1])
|
||||
@highlightRange(range)
|
||||
|
||||
# Unhighlight previously highlighted lines
|
||||
clearHighlight: ->
|
||||
$(".#{@highlightClass}").removeClass(@highlightClass)
|
||||
|
||||
# Convert a URL hash String into line numbers
|
||||
#
|
||||
# hash - Hash String
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# hashToRange('#L5') # => [5, null]
|
||||
# hashToRange('#L5-15') # => [5, 15]
|
||||
# hashToRange('#foo') # => [null, null]
|
||||
#
|
||||
# Returns an Array
|
||||
hashToRange: (hash) ->
|
||||
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/)
|
||||
|
||||
if matches && matches.length
|
||||
first = parseInt(matches[1])
|
||||
last = if matches[2] then parseInt(matches[2]) else null
|
||||
|
||||
[first, last]
|
||||
else
|
||||
[null, null]
|
||||
|
||||
# Highlight a single line
|
||||
#
|
||||
# lineNumber - Line number to highlight
|
||||
highlightLine: (lineNumber) =>
|
||||
$("#LC#{lineNumber}").addClass(@highlightClass)
|
||||
|
||||
# Highlight all lines within a range
|
||||
#
|
||||
# range - Array containing the starting and ending line numbers
|
||||
highlightRange: (range) ->
|
||||
if range[1]
|
||||
for lineNumber in [range[0]..range[1]]
|
||||
@highlightLine(lineNumber)
|
||||
else
|
||||
@highlightLine(range[0])
|
||||
|
||||
# Set the URL hash string
|
||||
setHash: (firstLineNumber, lastLineNumber) =>
|
||||
if lastLineNumber
|
||||
hash = "#L#{firstLineNumber}-#{lastLineNumber}"
|
||||
else
|
||||
hash = "#L#{firstLineNumber}"
|
||||
|
||||
@_hash = hash
|
||||
@__setLocationHash__(hash)
|
||||
|
||||
# Make the actual hash change in the browser
|
||||
#
|
||||
# This method is stubbed in tests.
|
||||
__setLocationHash__: (value) ->
|
||||
# We're using pushState instead of assigning location.hash directly to
|
||||
# prevent the page from scrolling on the hashchange event
|
||||
history.pushState({turbolinks: false, url: value}, document.title, value)
|
||||
@@ -24,7 +24,7 @@ class PasswordsController < Devise::PasswordsController
|
||||
super do |resource|
|
||||
# TODO (rspeicher): In Devise master (> 3.4.1), we can set
|
||||
# `Devise.sign_in_after_reset_password = false` and avoid this mess.
|
||||
if resource.errors.empty? && resource.try(:otp_required_for_login?)
|
||||
if resource.errors.empty? && resource.try(:two_factor_enabled?)
|
||||
resource.unlock_access! if unlockable?(resource)
|
||||
|
||||
# Since we are not signing this user in, we use the :updated_not_active
|
||||
|
||||
@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
|
||||
def create
|
||||
if current_user.valid_otp?(params[:pin_code])
|
||||
current_user.otp_required_for_login = true
|
||||
current_user.two_factor_enabled = true
|
||||
@codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
|
||||
@@ -30,7 +30,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
|
||||
def destroy
|
||||
current_user.update_attributes({
|
||||
otp_required_for_login: false,
|
||||
two_factor_enabled: false,
|
||||
encrypted_otp_secret: nil,
|
||||
encrypted_otp_secret_iv: nil,
|
||||
encrypted_otp_secret_salt: nil,
|
||||
|
||||
@@ -57,7 +57,7 @@ class SessionsController < Devise::SessionsController
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
return unless user && user.otp_required_for_login
|
||||
return unless user && user.two_factor_enabled?
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
if valid_otp_attempt?(user)
|
||||
|
||||
@@ -263,7 +263,7 @@ class Ability
|
||||
:"modify_#{name}",
|
||||
]
|
||||
else
|
||||
if subject.respond_to?(:project)
|
||||
if subject.respond_to?(:project) && subject.project
|
||||
project_abilities(user, subject.project)
|
||||
else
|
||||
[]
|
||||
|
||||
+15
-4
@@ -172,6 +172,9 @@ class User < ActiveRecord::Base
|
||||
after_create :post_create_hook
|
||||
after_destroy :post_destroy_hook
|
||||
|
||||
# User's Dashboard preference
|
||||
# Note: When adding an option, it MUST go on the end of the array.
|
||||
enum dashboard: [:projects, :stars]
|
||||
|
||||
alias_attribute :private_token, :authentication_token
|
||||
|
||||
@@ -297,6 +300,18 @@ class User < ActiveRecord::Base
|
||||
@reset_token
|
||||
end
|
||||
|
||||
# Check if the user has enabled Two-factor Authentication
|
||||
def two_factor_enabled?
|
||||
otp_required_for_login
|
||||
end
|
||||
|
||||
# Set whether or not Two-factor Authentication is enabled for the current user
|
||||
#
|
||||
# setting - Boolean
|
||||
def two_factor_enabled=(setting)
|
||||
self.otp_required_for_login = setting
|
||||
end
|
||||
|
||||
def namespace_uniq
|
||||
namespace_name = self.username
|
||||
existing_namespace = Namespace.by_path(namespace_name)
|
||||
@@ -704,8 +719,4 @@ class User < ActiveRecord::Base
|
||||
def can_be_removed?
|
||||
!solo_owned_groups.present?
|
||||
end
|
||||
|
||||
# User's Dashboard preference
|
||||
# Note: When adding an option, it MUST go on the end of the array.
|
||||
enum dashboard: [:projects, :stars]
|
||||
end
|
||||
|
||||
@@ -50,6 +50,14 @@
|
||||
= link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
|
||||
%i.fa.fa-times
|
||||
|
||||
%li.two-factor-status
|
||||
%span.light Two-factor Authentication:
|
||||
%strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'}
|
||||
- if @user.two_factor_enabled?
|
||||
Enabled
|
||||
- else
|
||||
Disabled
|
||||
|
||||
%li
|
||||
%span.light Can create groups:
|
||||
%strong
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
.panel-heading
|
||||
Two-factor Authentication
|
||||
.panel-body
|
||||
- if current_user.otp_required_for_login
|
||||
- if current_user.two_factor_enabled?
|
||||
.pull-right
|
||||
= link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
|
||||
data: { confirm: 'Are you sure?' }
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
- if issuable.is_a?(MergeRequest)
|
||||
%p.help-block
|
||||
- if issuable.work_in_progress?
|
||||
Remove the <code>WIP</code> prefix from the title to allow this
|
||||
Remove the <code>WIP</code> prefix from the title to allow this
|
||||
<strong>Work In Progress</strong> merge request to be accepted when it's ready.
|
||||
- else
|
||||
Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a
|
||||
Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a
|
||||
<strong>Work In Progress</strong> merge request from being accepted before it's ready.
|
||||
.form-group.issuable-description
|
||||
= f.label :description, 'Description', class: 'control-label'
|
||||
@@ -81,21 +81,22 @@
|
||||
|
||||
- if issuable.is_a?(MergeRequest)
|
||||
%hr
|
||||
- unless @merge_request.persisted?
|
||||
- if @merge_request.new_record?
|
||||
.form-group
|
||||
= f.label :source_branch, class: 'control-label' do
|
||||
%i.fa.fa-code-fork
|
||||
Source Branch
|
||||
.col-sm-10
|
||||
= f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
|
||||
%p.help-block
|
||||
= link_to 'Change source branch', mr_change_branches_path(@merge_request)
|
||||
.form-group
|
||||
= f.label :target_branch, class: 'control-label' do
|
||||
%i.fa.fa-code-fork
|
||||
Target Branch
|
||||
.col-sm-10
|
||||
= f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2' })
|
||||
= f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? })
|
||||
- if @merge_request.new_record?
|
||||
%p.help-block
|
||||
= link_to 'Change branches', mr_change_branches_path(@merge_request)
|
||||
|
||||
.form-actions
|
||||
- if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted?
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.file-content.code{class: user_color_scheme_class}
|
||||
.line-numbers
|
||||
- if blob.data.present?
|
||||
- blob.data.lines.to_a.size.times do |index|
|
||||
- blob.data.lines.each_index do |index|
|
||||
- offset = defined?(first_line_number) ? first_line_number : 1
|
||||
- i = index + offset
|
||||
/ We're not using `link_to` because it is too slow once we get to thousands of lines.
|
||||
%a{href: "#L#{i}", id: "L#{i}", rel: "#L#{i}"}
|
||||
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
|
||||
%a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
|
||||
%i.fa.fa-link
|
||||
= i
|
||||
:preserve
|
||||
|
||||
@@ -165,13 +165,18 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
|
||||
Sometimes during version upgrades you might end up with some wrong CSS or
|
||||
missing some icons. In that case, try to precompile the assets again.
|
||||
|
||||
For Omnibus-packages:
|
||||
```
|
||||
sudo gitlab-rake assets:precompile
|
||||
```
|
||||
Note that this only applies to source installations and does NOT apply to
|
||||
omnibus packages.
|
||||
|
||||
For installations from source:
|
||||
```
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
|
||||
```
|
||||
|
||||
For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
|
||||
the release of upstream GitLab. The omnibus version includes optimized versions
|
||||
of those assets. Unless you are modifying the JavaScript / CSS code on your
|
||||
production machine after installing the package, there should be no reason to redo
|
||||
rake assets:precompile on the production machine. If you suspect that assets
|
||||
have been corrupted, you should reinstall the omnibus package.
|
||||
|
||||
+29
-1
@@ -30,7 +30,7 @@ cat ~/.ssh/id_rsa.pub
|
||||
|
||||
Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
|
||||
user profile. Please copy the complete key starting with `ssh-` and ending
|
||||
with your username and host.
|
||||
with your username and host.
|
||||
|
||||
Use code below to copy your public key to the clipboard. Depending on your
|
||||
OS you'll need to use a different command:
|
||||
@@ -77,3 +77,31 @@ information.
|
||||
### Eclipse
|
||||
|
||||
How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
|
||||
|
||||
## Tip: Non-default OpenSSH key file names or locations
|
||||
|
||||
If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
|
||||
|
||||
```
|
||||
#
|
||||
# Main gitlab.com server
|
||||
#
|
||||
Host gitlab.com
|
||||
RSAAuthentication yes
|
||||
IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename
|
||||
User mygitlabusername
|
||||
```
|
||||
|
||||
Another example
|
||||
```
|
||||
#
|
||||
# Our company's internal Gitlab server
|
||||
#
|
||||
Host my-gitlab.company.com
|
||||
RSAAuthentication yes
|
||||
IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename
|
||||
```
|
||||
|
||||
Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ.
|
||||
|
||||
Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document.
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
Feature: Project Source Multiselect Blob
|
||||
Background:
|
||||
Given I sign in as a user
|
||||
And I own project "Shop"
|
||||
And I visit ".gitignore" file in repo
|
||||
|
||||
@javascript
|
||||
Scenario: I click line 1 in file
|
||||
When I click line 1 in file
|
||||
Then I should see "L1" as URI fragment
|
||||
And I should see line 1 highlighted
|
||||
|
||||
@javascript
|
||||
Scenario: I shift-click line 1 in file
|
||||
When I shift-click line 1 in file
|
||||
Then I should see "L1" as URI fragment
|
||||
And I should see line 1 highlighted
|
||||
|
||||
@javascript
|
||||
Scenario: I click line 1 then click line 2 in file
|
||||
When I click line 1 in file
|
||||
Then I should see "L1" as URI fragment
|
||||
And I should see line 1 highlighted
|
||||
Then I click line 2 in file
|
||||
Then I should see "L2" as URI fragment
|
||||
And I should see line 2 highlighted
|
||||
|
||||
@javascript
|
||||
Scenario: I click various line numbers to test multiselect
|
||||
Then I click line 1 in file
|
||||
Then I should see "L1" as URI fragment
|
||||
And I should see line 1 highlighted
|
||||
Then I shift-click line 2 in file
|
||||
Then I should see "L1-2" as URI fragment
|
||||
And I should see lines 1-2 highlighted
|
||||
Then I shift-click line 3 in file
|
||||
Then I should see "L1-3" as URI fragment
|
||||
And I should see lines 1-3 highlighted
|
||||
Then I click line 3 in file
|
||||
Then I should see "L3" as URI fragment
|
||||
And I should see line 3 highlighted
|
||||
Then I shift-click line 1 in file
|
||||
Then I should see "L1-3" as URI fragment
|
||||
And I should see lines 1-3 highlighted
|
||||
Then I shift-click line 5 in file
|
||||
Then I should see "L1-5" as URI fragment
|
||||
And I should see lines 1-5 highlighted
|
||||
Then I shift-click line 4 in file
|
||||
Then I should see "L1-4" as URI fragment
|
||||
And I should see lines 1-4 highlighted
|
||||
Then I click line 5 in file
|
||||
Then I should see "L5" as URI fragment
|
||||
And I should see line 5 highlighted
|
||||
Then I shift-click line 3 in file
|
||||
Then I should see "L3-5" as URI fragment
|
||||
And I should see lines 3-5 highlighted
|
||||
Then I shift-click line 1 in file
|
||||
Then I should see "L1-3" as URI fragment
|
||||
And I should see lines 1-3 highlighted
|
||||
Then I shift-click line 1 in file
|
||||
Then I should see "L1" as URI fragment
|
||||
And I should see line 1 highlighted
|
||||
|
||||
@javascript
|
||||
Scenario: I multiselect lines 1-5 and then go back and forward in history
|
||||
When I click line 1 in file
|
||||
And I shift-click line 3 in file
|
||||
And I shift-click line 2 in file
|
||||
And I shift-click line 5 in file
|
||||
Then I should see "L1-5" as URI fragment
|
||||
And I should see lines 1-5 highlighted
|
||||
Then I go back in history
|
||||
Then I should see "L1-2" as URI fragment
|
||||
And I should see lines 1-2 highlighted
|
||||
Then I go back in history
|
||||
Then I should see "L1-3" as URI fragment
|
||||
And I should see lines 1-3 highlighted
|
||||
Then I go back in history
|
||||
Then I should see "L1" as URI fragment
|
||||
And I should see line 1 highlighted
|
||||
Then I go forward in history
|
||||
And I go forward in history
|
||||
And I go forward in history
|
||||
Then I should see "L1-5" as URI fragment
|
||||
And I should see lines 1-5 highlighted
|
||||
@@ -25,4 +25,15 @@ Feature: Snippets
|
||||
Scenario: I destroy "Personal snippet one"
|
||||
Given I visit snippet page "Personal snippet one"
|
||||
And I click link "Destroy"
|
||||
Then I should not see "Personal snippet one" in snippets
|
||||
Then I should not see "Personal snippet one" in snippets
|
||||
|
||||
Scenario: I create new internal snippet
|
||||
Given I logout directly
|
||||
And I sign in as an admin
|
||||
Then I visit new snippet page
|
||||
And I submit new internal snippet
|
||||
Then I visit snippet page "Internal personal snippet one"
|
||||
And I logout directly
|
||||
Then I sign in as a user
|
||||
Given I visit new snippet page
|
||||
Then I visit snippet page "Internal personal snippet one"
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
class Spinach::Features::ProjectSourceMultiselectBlob < Spinach::FeatureSteps
|
||||
include SharedAuthentication
|
||||
include SharedProject
|
||||
include SharedPaths
|
||||
|
||||
class << self
|
||||
def click_line_steps(*line_numbers)
|
||||
line_numbers.each do |line_number|
|
||||
step "I click line #{line_number} in file" do
|
||||
find("#L#{line_number}").click
|
||||
end
|
||||
|
||||
step "I shift-click line #{line_number} in file" do
|
||||
script = "$('#L#{line_number}').trigger($.Event('click', { shiftKey: true }));"
|
||||
execute_script(script)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_state_steps(*ranges)
|
||||
ranges.each do |range|
|
||||
fragment = range.kind_of?(Array) ? "L#{range.first}-#{range.last}" : "L#{range}"
|
||||
pluralization = range.kind_of?(Array) ? "s" : ""
|
||||
|
||||
step "I should see \"#{fragment}\" as URI fragment" do
|
||||
expect(URI.parse(current_url).fragment).to eq fragment
|
||||
end
|
||||
|
||||
step "I should see line#{pluralization} #{fragment[1..-1]} highlighted" do
|
||||
ids = Array(range).map { |n| "LC#{n}" }
|
||||
extra = false
|
||||
|
||||
highlighted = page.all("#tree-content-holder .highlight .line.hll")
|
||||
highlighted.each do |element|
|
||||
extra ||= ids.delete(element[:id]).nil?
|
||||
end
|
||||
|
||||
expect(extra).to be_false and ids.should be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
click_line_steps *Array(1..5)
|
||||
check_state_steps *Array(1..5), Array(1..2), Array(1..3), Array(1..4), Array(1..5), Array(3..5)
|
||||
|
||||
step 'I go back in history' do
|
||||
go_back
|
||||
end
|
||||
|
||||
step 'I go forward in history' do
|
||||
go_forward
|
||||
end
|
||||
|
||||
step 'I click on ".gitignore" file in repo' do
|
||||
click_link ".gitignore"
|
||||
end
|
||||
end
|
||||
@@ -28,6 +28,10 @@ module SharedAuthentication
|
||||
logout
|
||||
end
|
||||
|
||||
step "I logout directly" do
|
||||
logout_direct
|
||||
end
|
||||
|
||||
def current_user
|
||||
@user || User.first
|
||||
end
|
||||
|
||||
@@ -31,6 +31,18 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
|
||||
click_button "Create snippet"
|
||||
end
|
||||
|
||||
step 'I submit new internal snippet' do
|
||||
fill_in "personal_snippet_title", :with => "Internal personal snippet one"
|
||||
fill_in "personal_snippet_file_name", :with => "my_snippet.rb"
|
||||
choose 'personal_snippet_visibility_level_10'
|
||||
|
||||
page.within('.file-editor') do
|
||||
find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of internal snippet'
|
||||
end
|
||||
|
||||
click_button "Create snippet"
|
||||
end
|
||||
|
||||
step 'I should see snippet "Personal snippet three"' do
|
||||
expect(page).to have_content "Personal snippet three"
|
||||
expect(page).to have_content "Content of snippet three"
|
||||
@@ -58,7 +70,15 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
|
||||
visit snippet_path(snippet)
|
||||
end
|
||||
|
||||
step 'I visit snippet page "Internal personal snippet one"' do
|
||||
visit snippet_path(internal_snippet)
|
||||
end
|
||||
|
||||
def snippet
|
||||
@snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
|
||||
end
|
||||
|
||||
def internal_snippet
|
||||
@snippet ||= PersonalSnippet.find_by!(title: "Internal personal snippet one")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,11 +40,11 @@ describe Profiles::TwoFactorAuthsController do
|
||||
expect(user).to receive(:valid_otp?).with(pin).and_return(true)
|
||||
end
|
||||
|
||||
it 'sets otp_required_for_login' do
|
||||
it 'sets two_factor_enabled' do
|
||||
go
|
||||
|
||||
user.reload
|
||||
expect(user.otp_required_for_login).to eq true
|
||||
expect(user).to be_two_factor_enabled
|
||||
end
|
||||
|
||||
it 'presents plaintext codes for the user to save' do
|
||||
@@ -109,13 +109,13 @@ describe Profiles::TwoFactorAuthsController do
|
||||
let!(:codes) { user.generate_otp_backup_codes! }
|
||||
|
||||
it 'clears all 2FA-related fields' do
|
||||
expect(user.otp_required_for_login).to eq true
|
||||
expect(user).to be_two_factor_enabled
|
||||
expect(user.otp_backup_codes).not_to be_nil
|
||||
expect(user.encrypted_otp_secret).not_to be_nil
|
||||
|
||||
delete :destroy
|
||||
|
||||
expect(user.otp_required_for_login).to eq false
|
||||
expect(user).not_to be_two_factor_enabled
|
||||
expect(user.otp_backup_codes).to be_nil
|
||||
expect(user.encrypted_otp_secret).to be_nil
|
||||
end
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ FactoryGirl.define do
|
||||
|
||||
trait :two_factor do
|
||||
before(:create) do |user|
|
||||
user.otp_required_for_login = true
|
||||
user.two_factor_enabled = true
|
||||
user.otp_secret = User.generate_otp_secret(32)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -63,15 +63,35 @@ describe "Admin::Users", feature: true do
|
||||
end
|
||||
|
||||
describe "GET /admin/users/:id" do
|
||||
before do
|
||||
visit admin_users_path
|
||||
click_link "#{@user.name}"
|
||||
end
|
||||
|
||||
it "should have user info" do
|
||||
visit admin_users_path
|
||||
click_link @user.name
|
||||
|
||||
expect(page).to have_content(@user.email)
|
||||
expect(page).to have_content(@user.name)
|
||||
end
|
||||
|
||||
describe 'Two-factor Authentication status' do
|
||||
it 'shows when enabled' do
|
||||
@user.update_attribute(:two_factor_enabled, true)
|
||||
|
||||
visit admin_user_path(@user)
|
||||
|
||||
expect_two_factor_status('Enabled')
|
||||
end
|
||||
|
||||
it 'shows when disabled' do
|
||||
visit admin_user_path(@user)
|
||||
|
||||
expect_two_factor_status('Disabled')
|
||||
end
|
||||
|
||||
def expect_two_factor_status(status)
|
||||
page.within('.two-factor-status') do
|
||||
expect(page).to have_content(status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /admin/users/:id/edit" do
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#tree-content-holder
|
||||
.file-content
|
||||
.line-numbers
|
||||
- 1.upto(25) do |i|
|
||||
%a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i
|
||||
%pre.code.highlight
|
||||
%code
|
||||
- 1.upto(25) do |i|
|
||||
%span.line{id: "LC#{i}"}= "Line #{i}"
|
||||
@@ -0,0 +1,150 @@
|
||||
#= require line_highlighter
|
||||
|
||||
describe 'LineHighlighter', ->
|
||||
fixture.preload('line_highlighter.html')
|
||||
|
||||
clickLine = (number, eventData = {}) ->
|
||||
if $.isEmptyObject(eventData)
|
||||
$("#L#{number}").mousedown().click()
|
||||
else
|
||||
e = $.Event 'mousedown', eventData
|
||||
$("#L#{number}").trigger(e).click()
|
||||
|
||||
beforeEach ->
|
||||
fixture.load('line_highlighter.html')
|
||||
@class = new LineHighlighter()
|
||||
@css = @class.highlightClass
|
||||
@spies = {
|
||||
__setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake ->
|
||||
}
|
||||
|
||||
describe 'behavior', ->
|
||||
it 'highlights one line given in the URL hash', ->
|
||||
new LineHighlighter('#L13')
|
||||
expect($('#LC13')).toHaveClass(@css)
|
||||
|
||||
it 'highlights a range of lines given in the URL hash', ->
|
||||
new LineHighlighter('#L5-25')
|
||||
expect($(".#{@css}").length).toBe(21)
|
||||
expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25]
|
||||
|
||||
it 'scrolls to the first highlighted line on initial load', ->
|
||||
spy = spyOn($, 'scrollTo')
|
||||
new LineHighlighter('#L5-25')
|
||||
expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything())
|
||||
|
||||
it 'discards click events', ->
|
||||
spy = spyOnEvent('a[data-line-number]', 'click')
|
||||
clickLine(13)
|
||||
expect(spy).toHaveBeenPrevented()
|
||||
|
||||
it 'handles garbage input from the hash', ->
|
||||
func = -> new LineHighlighter('#tree-content-holder')
|
||||
expect(func).not.toThrow()
|
||||
|
||||
describe '#clickHandler', ->
|
||||
it 'discards the mousedown event', ->
|
||||
spy = spyOnEvent('a[data-line-number]', 'mousedown')
|
||||
clickLine(13)
|
||||
expect(spy).toHaveBeenPrevented()
|
||||
|
||||
describe 'without shiftKey', ->
|
||||
it 'highlights one line when clicked', ->
|
||||
clickLine(13)
|
||||
expect($('#LC13')).toHaveClass(@css)
|
||||
|
||||
it 'unhighlights previously highlighted lines', ->
|
||||
clickLine(13)
|
||||
clickLine(20)
|
||||
|
||||
expect($('#LC13')).not.toHaveClass(@css)
|
||||
expect($('#LC20')).toHaveClass(@css)
|
||||
|
||||
it 'sets the hash', ->
|
||||
spy = spyOn(@class, 'setHash').and.callThrough()
|
||||
clickLine(13)
|
||||
expect(spy).toHaveBeenCalledWith(13)
|
||||
|
||||
describe 'with shiftKey', ->
|
||||
it 'sets the hash', ->
|
||||
spy = spyOn(@class, 'setHash').and.callThrough()
|
||||
clickLine(13)
|
||||
clickLine(20, shiftKey: true)
|
||||
expect(spy).toHaveBeenCalledWith(13)
|
||||
expect(spy).toHaveBeenCalledWith(13, 20)
|
||||
|
||||
describe 'without existing highlight', ->
|
||||
it 'highlights the clicked line', ->
|
||||
clickLine(13, shiftKey: true)
|
||||
expect($('#LC13')).toHaveClass(@css)
|
||||
expect($(".#{@css}").length).toBe(1)
|
||||
|
||||
it 'sets the hash', ->
|
||||
spy = spyOn(@class, 'setHash')
|
||||
clickLine(13, shiftKey: true)
|
||||
expect(spy).toHaveBeenCalledWith(13)
|
||||
|
||||
describe 'with existing single-line highlight', ->
|
||||
it 'uses existing line as last line when target is lesser', ->
|
||||
clickLine(20)
|
||||
clickLine(15, shiftKey: true)
|
||||
expect($(".#{@css}").length).toBe(6)
|
||||
expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20]
|
||||
|
||||
it 'uses existing line as first line when target is greater', ->
|
||||
clickLine(5)
|
||||
clickLine(10, shiftKey: true)
|
||||
expect($(".#{@css}").length).toBe(6)
|
||||
expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10]
|
||||
|
||||
describe 'with existing multi-line highlight', ->
|
||||
beforeEach ->
|
||||
clickLine(10, shiftKey: true)
|
||||
clickLine(13, shiftKey: true)
|
||||
|
||||
it 'uses target as first line when it is less than existing first line', ->
|
||||
clickLine(5, shiftKey: true)
|
||||
expect($(".#{@css}").length).toBe(6)
|
||||
expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10]
|
||||
|
||||
it 'uses target as last line when it is greater than existing first line', ->
|
||||
clickLine(15, shiftKey: true)
|
||||
expect($(".#{@css}").length).toBe(6)
|
||||
expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15]
|
||||
|
||||
describe '#hashToRange', ->
|
||||
beforeEach ->
|
||||
@subject = @class.hashToRange
|
||||
|
||||
it 'extracts a single line number from the hash', ->
|
||||
expect(@subject('#L5')).toEqual([5, null])
|
||||
|
||||
it 'extracts a range of line numbers from the hash', ->
|
||||
expect(@subject('#L5-15')).toEqual([5, 15])
|
||||
|
||||
it 'returns [null, null] when the hash is not a line number', ->
|
||||
expect(@subject('#foo')).toEqual([null, null])
|
||||
|
||||
describe '#highlightLine', ->
|
||||
beforeEach ->
|
||||
@subject = @class.highlightLine
|
||||
|
||||
it 'highlights the specified line', ->
|
||||
@subject(13)
|
||||
expect($('#LC13')).toHaveClass(@css)
|
||||
|
||||
it 'accepts a String-based number', ->
|
||||
@subject('13')
|
||||
expect($('#LC13')).toHaveClass(@css)
|
||||
|
||||
describe '#setHash', ->
|
||||
beforeEach ->
|
||||
@subject = @class.setHash
|
||||
|
||||
it 'sets the location hash for a single line', ->
|
||||
@subject(5)
|
||||
expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5')
|
||||
|
||||
it 'sets the location hash for a range', ->
|
||||
@subject(5, 15)
|
||||
expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15')
|
||||
@@ -210,6 +210,30 @@ describe User do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#two_factor_enabled' do
|
||||
it 'returns two-factor authentication status' do
|
||||
enabled = build_stubbed(:user, two_factor_enabled: true)
|
||||
disabled = build_stubbed(:user)
|
||||
|
||||
expect(enabled).to be_two_factor_enabled
|
||||
expect(disabled).not_to be_two_factor_enabled
|
||||
end
|
||||
end
|
||||
|
||||
describe '#two_factor_enabled=' do
|
||||
it 'enables two-factor authentication' do
|
||||
user = build_stubbed(:user, two_factor_enabled: false)
|
||||
expect { user.two_factor_enabled = true }.
|
||||
to change { user.two_factor_enabled? }.to(true)
|
||||
end
|
||||
|
||||
it 'disables two-factor authentication' do
|
||||
user = build_stubbed(:user, two_factor_enabled: true)
|
||||
expect { user.two_factor_enabled = false }.
|
||||
to change { user.two_factor_enabled? }.to(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'authentication token' do
|
||||
it "should have authentication token" do
|
||||
user = create(:user)
|
||||
|
||||
@@ -39,4 +39,9 @@ module LoginHelpers
|
||||
def logout
|
||||
find(:css, ".fa.fa-sign-out").click
|
||||
end
|
||||
|
||||
# Logout without JavaScript driver
|
||||
def logout_direct
|
||||
page.driver.submit :delete, '/users/sign_out', {}
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user