Commit 551c2aea authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into ce_upstream

parents c47074b6 b3e51096
......@@ -58,7 +58,7 @@ class ApplicationController < ActionController::Base
if current_user
not_found
else
redirect_to new_user_session_path
authenticate_user!
end
end
......
module RoutableActions
extend ActiveSupport::Concern
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable_klass, routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
route_not_found
nil
end
end
def routable_authorized?(routable_klass, routable, extra_authorization_proc)
action = :"read_#{routable_klass.to_s.underscore}"
return false unless can?(current_user, action, routable)
if extra_authorization_proc
extra_authorization_proc.call(routable)
else
true
end
end
def ensure_canonical_path(routable, requested_path)
return unless request.get?
canonical_path = routable.full_path
if canonical_path != requested_path
if canonical_path.casecmp(requested_path) != 0
flash[:notice] = "Project '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
end
redirect_to request.original_url.sub(requested_path, canonical_path)
end
end
end
class Groups::ApplicationController < ApplicationController
include RoutableActions
layout 'group'
skip_before_action :authenticate_user!
......@@ -7,29 +9,17 @@ class Groups::ApplicationController < ApplicationController
private
def group
unless @group
id = params[:group_id] || params[:id]
@group = Group.find_by_full_path(id)
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
unless @group && can?(current_user, :read_group, @group)
@group = nil
if current_user.nil?
authenticate_user!
else
render_404
end
end
end
@group
@group ||= find_routable!(Group, params[:group_id] || params[:id])
end
def group_projects
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def group_merge_requests
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
end
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
......
......@@ -12,8 +12,8 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :group_merge_requests, only: [:merge_requests]
before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups]
......
class Projects::ApplicationController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user!
before_action :redirect_git_extension
before_action :project
before_action :repository
layout 'project'
......@@ -8,40 +11,22 @@ class Projects::ApplicationController < ApplicationController
private
def redirect_git_extension
# Redirect from
# localhost/group/project.git
# to
# localhost/group/project
#
redirect_to url_for(params.merge(format: nil)) if params[:format] == 'git'
end
def project
unless @project
namespace = params[:namespace_id]
id = params[:project_id] || params[:id]
# Redirect from
# localhost/group/project.git
# to
# localhost/group/project
#
if params[:format] == 'git'
redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
return
end
project_path = "#{namespace}/#{id}"
@project = Project.find_by_full_path(project_path)
if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
end
else
@project = nil
if current_user.nil?
authenticate_user!
else
render_404
end
end
end
return @project if @project
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
@project
@project = find_routable!(Project, path, extra_authorization_proc: auth_proc)
end
def repository
......
class UsersController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
before_action :authorize_read_user!, only: [:show]
def show
respond_to do |format|
......@@ -91,12 +92,8 @@ class UsersController < ApplicationController
private
def authorize_read_user!
render_404 unless can?(current_user, :read_user, user)
end
def user
@user ||= User.find_by_username!(params[:username])
@user ||= find_routable!(User, params[:username])
end
def contributed_projects
......
......@@ -5,6 +5,7 @@ module Routable
included do
has_one :route, as: :source, autosave: true, dependent: :destroy
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy
validates_associated :route
validates :route, presence: true
......@@ -26,16 +27,31 @@ module Routable
# Klass.find_by_full_path('gitlab-org/gitlab-ce')
#
# Returns a single object, or nil.
def find_by_full_path(path)
def find_by_full_path(path, follow_redirects: false)
# 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.
#
# Why do we do this?
#
# Even though we have Rails validation on Route for unique paths
# (case-insensitive), there are old projects in our DB (and possibly
# clients' DBs) that have the same path with different cases.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/18603. Also note that
# our unique index is case-sensitive in Postgres.
binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
where_full_path_in([path]).reorder(order_sql).take
found = where_full_path_in([path]).reorder(order_sql).take
return found if found
if follow_redirects
if Gitlab::Database.postgresql?
joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path)
else
joins(:redirect_routes).find_by(redirect_routes: { path: path })
end
end
end
# Builds a relation to find multiple objects by their full paths.
......
class RedirectRoute < ActiveRecord::Base
belongs_to :source, polymorphic: true
validates :source, presence: true
validates :path,
length: { within: 1..255 },
presence: true,
uniqueness: { case_sensitive: false }
scope :matching_path_and_descendants, -> (path) { where('redirect_routes.path = ? OR redirect_routes.path LIKE ?', path, "#{sanitize_sql_like(path)}/%") }
end
......@@ -8,29 +8,58 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
after_update :rename_descendants
scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") }
def rename_descendants
if path_changed? || name_changed?
descendants = self.class.inside_path(path_was)
return unless path_changed? || name_changed?
descendants.each do |route|
attributes = {}
descendant_routes = self.class.inside_path(path_was)
if path_changed? && route.path.present?
attributes[:path] = route.path.sub(path_was, path)
end
descendant_routes.each do |route|
attributes = {}
if name_changed? && name_was.present? && route.name.present?
attributes[:name] = route.name.sub(name_was, name)
end
if path_changed? && route.path.present?
attributes[:path] = route.path.sub(path_was, path)
end
# Note that update_columns skips validation and callbacks.
# We need this to avoid recursive call of rename_descendants method
route.update_columns(attributes) unless attributes.empty?
if name_changed? && name_was.present? && route.name.present?
attributes[:name] = route.name.sub(name_was, name)
end
if attributes.present?
old_path = route.path
# Callbacks must be run manually
route.update_columns(attributes)
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants.
route.create_redirect(old_path) if attributes[:path]
end
end
end
def delete_conflicting_redirects
conflicting_redirects.delete_all
end
def conflicting_redirects
RedirectRoute.matching_path_and_descendants(path)
end
def create_redirect(path)
RedirectRoute.create(source: source, path: path)
end
private
def create_redirect_for_old_path
create_redirect(path_was) if path_changed?
end
end
......@@ -359,6 +359,11 @@ class User < ActiveRecord::Base
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end
def find_by_full_path(path, follow_redirects: false)
namespace = Namespace.find_by_full_path(path, follow_redirects: follow_redirects)
namespace&.owner
end
def non_ldap
joins('LEFT JOIN identities ON identities.user_id = users.id')
.where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%')
......@@ -386,6 +391,10 @@ class User < ActiveRecord::Base
end
end
def full_path
username
end
def self.internal_attributes
[:ghost]
end
......
---
title: Redirect old links after renaming a user/group/project.
merge_request: 10370
author:
class CreateRedirectRoutes < ActiveRecord::Migration
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :redirect_routes do |t|
t.integer :source_id, null: false
t.string :source_type, null: false
t.string :path, null: false
t.timestamps null: false
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToRedirectRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:redirect_routes, :path, unique: true)
add_concurrent_index(:redirect_routes, [:source_type, :source_id])
end
def down
remove_concurrent_index(:redirect_routes, :path) if index_exists?(:redirect_routes, :path)
remove_concurrent_index(:redirect_routes, [:source_type, :source_id]) if index_exists?(:redirect_routes, [:source_type, :source_id])
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class IndexRedirectRoutesPathForLike < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
INDEX_NAME = 'index_redirect_routes_on_path_text_pattern_ops'
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
unless index_exists?(:redirect_routes, :path, name: INDEX_NAME)
execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (path varchar_pattern_ops);")
end
end
def down
return unless Gitlab::Database.postgresql?
if index_exists?(:redirect_routes, :path, name: INDEX_NAME)
execute("DROP INDEX CONCURRENTLY #{INDEX_NAME};")
end
end
end
......@@ -11,7 +11,11 @@
#
# It's strongly recommended that you check this file into your version control system.
<<<<<<< HEAD
ActiveRecord::Schema.define(version: 20170504102911) do
=======
ActiveRecord::Schema.define(version: 20170503185032) do
>>>>>>> b3e5109689a971c2574da51a1683d5e424a31564
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1233,6 +1237,18 @@ ActiveRecord::Schema.define(version: 20170504102911) do
add_index "push_rules", ["project_id"], name: "index_push_rules_on_project_id", using: :btree
create_table "redirect_routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
create_table "releases", force: :cascade do |t|
t.string "tag"
t.text "description"
......
......@@ -67,11 +67,8 @@ Manage files and branches from the UI (user interface):
### Issues and Merge Requests (MRs)
- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- Issues
- [Create an issue](gitlab-basics/create-issue.md#how-to-create-an-issue-in-gitlab)
- [Confidential Issues](user/project/issues/confidential_issues.md)
- [Automatic issue closing](user/project/issues/automatic_issue_closing.md)
- [Issue Boards](user/project/issue_board.md)
- [Issues](user/project/issues/index.md)
- [Issue Board](user/project/issue_board.md)
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
- [Merge Requests](user/project/merge_requests/index.md)
......
......@@ -11,5 +11,5 @@ Step-by-step guides on the basics of working with Git and GitLab.
- [Fork a project](fork-project.md)
- [Add a file](add-file.md)
- [Add an image](add-image.md)
- [Create an issue](create-issue.md)
- [Create an issue](../user/project/issues/create_new_issue.md)
- [Create a merge request](add-merge-request.md)
# How to create an Issue in GitLab
The issue tracker is a good place to add things that need to be improved or
solved in a project.
---
1. Go to the project where you'd like to create the issue and navigate to the
**Issues** tab on top.
![Issues](img/project_navbar.png)
1. Click on the **New issue** button on the right side of your screen.
![New issue](img/new_issue_button.png)
1. At the very minimum, add a title and a description to your issue.
You may assign it to a user, add a milestone or add labels (all optional).
![Issue title and description](img/new_issue_page.png)
1. When ready, click on **Submit issue**.
---
Your Issue will now be added to the issue tracker of the project you opened it
at and will be ready to be reviewed. You can comment on it and mention the
people involved. You can also link issues to the merge requests where the issues
are solved. To do this, you can use an
[issue closing pattern](../user/project/issues/automatic_issue_closing.md).
This document was moved to [another location](../user/project/issues/index.md#new-issue).
......@@ -11,7 +11,7 @@ Create projects and groups.
Create issues, labels, milestones, cast your vote, and review issues.
- [Create a new issue](../gitlab-basics/create-issue.md)
- [Create an issue](../user/project/issues/create_new_issue.md)
- [Assign labels to issues](../user/project/labels.md)
- [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md)
- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
......
# Closing Issues
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
## Directly
Whenever you decide that's no longer need for that issue,
close the issue using the close button:
![close issue - button](img/button_close_issue.png)
## Via Merge Request
When a merge request resolves the discussion over an issue, you can
make it close that issue(s) when merged.
All you need is to use a [keyword](automatic_issue_closing.md)
accompanying the issue number, add to the description of that MR.
In this example, the keyword "closes" prefixing the issue number will create a relationship
in such a way that the merge request will close the issue when merged.
Mentioning various issues in the same line also works for this purpose:
```md
Closes #333, #444, #555 and #666
```
If the issue is in a different repository rather then the MR's,
add the full URL for that issue(s):
```md
Closes #333, #444, and https://gitlab.com/<username>/<projectname>/issues/<xxx>
```
All the following keywords will produce the same behaviour:
- Close, Closes, Closed, Closing, close, closes, closed, closing
- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
![merge request closing issue when merged](img/merge_request_closes_issue.png)
If you use any other word before the issue number, the issue and the MR will
link to each other, but the MR will NOT close the issue(s) when merged.
![mention issues in MRs - closing and related](img/closing_and_related_issues.png)
## From the Issue Board
You can close an issue from [Issue Boards](../issue_board.md) by draging an issue card
from its list and dropping into **Closed**.
![close issue from the Issue Board](img/close_issue_from_board.gif)
## Customizing the issue closing patern
Alternatively, a GitLab **administrator** can
[customize the issue closing patern](../../../administration/issue_closing_pattern.md).
# Create a new Issue
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
When you create a new issue, you'll be prompted to fill in
the information illustrated on the image below.
![New issue from the issues list](img/new_issue.png)
Read through the [issues functionalities documentation](issues_functionalities.md#issues-functionalities)
to understand these fields one by one.
## New issue from the Issue Tracker
Navigate to your **Project's Dashboard** > **Issues** > **New Issue** to create a new issue:
![New issue from the issue list view](img/new_issue_from_tracker_list.png)
## New issue from an opened issue
From an **opened issue** in your project, click **New Issue** to create a new
issue in the same project:
![New issue from an open issue](img/new_issue_from_open_issue.png)
## New issue from the project's dashboard
From your **Project's Dashboard**, click the plus sign (**+**) to open a dropdown
menu with a few options. Select **New Issue** to create an issue in that project:
![New issue from a project's dashboard](img/new_issue_from_projects_dashboard.png)
## New issue from the Issue Board
From an Issue Board, create a new issue by clicking on the plus sign (**+**) on the top of a list.
It opens a new issue for that project labeled after its respective list.
![From the issue board](img/new_issue_from_issue_board.png)
# Crosslinking Issues
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
## From Commit Messages
Every time you mention an issue in your commit message, you're creating
a relationship between the two stages of the development workflow: the
issue itself and the first commit related to that issue.
If the issue and the code you're committing are both in the same project,
you simply add `#xxx` to the commit message, where `xxx` is the issue number.
If they are not in the same project, you can add the full URL to the issue
(`https://gitlab.com/<username>/<projectname>/issues/<xxx>`).
```shell
git commit -m "this is my commit message. Ref #xxx"
```
or
```shell
git commit -m "this is my commit message. Related to https://gitlab.com/<username>/<projectname>/issues/<xxx>"
```
Of course, you can replace `gitlab.com` with the URL of your own GitLab instance.
**Note:** Linking your first commit to your issue is going to be relevant
for tracking your process far ahead with
[GitLab Cycle Analytics](https://about.gitlab.com/features/cycle-analytics/)).
It will measure the time taken for planning the implementation of that issue,
which is the time between creating an issue and making the first commit.
## From Related Issues
Mentioning related issues in merge requests and other issues is useful
for your team members and collaborators to know that there are opened
issues around that same idea.
You do that as explained above, when
[mentioning an issue from a commit message](#from-commit-messages).
When mentioning the issue "A" in a issue "B", the issue "A" will also
display a notification in its tracker. The same is valid for mentioning
issues in merge requests.
![issue mentioned in issue](img/mention_in_issue.png)
## From Merge Requests
Mentioning issues in merge request comments work exactly the same way
they do for [related issues](#from-related-issues).
When you mention an issue in a merge request description, you can either
[close the issue as soon as the merge request is merged](closing_issues.md#via-merge-request),
or simply link both issue and merge request as described in the
[closing issues documentation](closing_issues.md#from-related-issues).
![issue mentioned in MR](img/mention_in_merge_request.png)
### Close an issue by merging a merge request
To [close an issue when a merge request is merged](closing_issues.md#via-merge-request), use the [automatic issue closing patern](automatic_issue_closing.md).
......@@ -2,6 +2,8 @@
> [Introduced][ce-3614] in GitLab 8.7.
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
Due dates can be used in issues to keep track of deadlines and make sure
features are shipped on time. Due dates require at least [Reporter permissions][permissions]
to be able to edit them. On the contrary, they can be seen by everybody.
......@@ -22,8 +24,8 @@ Changes are saved immediately.
## Making use of due dates
Issues that have a due date can be distinctively seen in the issues index page
with a calendar icon next to them. Issues where the date is past due will have
Issues that have a due date can be distinctively seen in the issue tracker
displaying a date next to them. Issues where the date is overdue will have
the icon and the date colored red. You can sort issues by those that are
_Due soon_ or _Due later_ from the dropdown menu in the right.
......
doc/user/project/issues/img/confidential_issues_create.png

9.43 KB | W: | H:

doc/user/project/issues/img/confidential_issues_create.png

7.99 KB | W: | H:

doc/user/project/issues/img/confidential_issues_create.png
doc/user/project/issues/img/confidential_issues_create.png
doc/user/project/issues/img/confidential_issues_create.png
doc/user/project/issues/img/confidential_issues_create.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/confidential_issues_index_page.png

9.72 KB | W: | H:

doc/user/project/issues/img/confidential_issues_index_page.png

8.15 KB | W: | H:

doc/user/project/issues/img/confidential_issues_index_page.png
doc/user/project/issues/img/confidential_issues_index_page.png
doc/user/project/issues/img/confidential_issues_index_page.png
doc/user/project/issues/img/confidential_issues_index_page.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/confidential_issues_issue_page.png

15.7 KB | W: | H:

doc/user/project/issues/img/confidential_issues_issue_page.png

13.9 KB | W: | H:

doc/user/project/issues/img/confidential_issues_issue_page.png
doc/user/project/issues/img/confidential_issues_issue_page.png
doc/user/project/issues/img/confidential_issues_issue_page.png
doc/user/project/issues/img/confidential_issues_issue_page.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/confidential_issues_search_guest.png

9.78 KB | W: | H:

doc/user/project/issues/img/confidential_issues_search_guest.png

8.39 KB | W: | H:

doc/user/project/issues/img/confidential_issues_search_guest.png
doc/user/project/issues/img/confidential_issues_search_guest.png
doc/user/project/issues/img/confidential_issues_search_guest.png
doc/user/project/issues/img/confidential_issues_search_guest.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/confidential_issues_system_notes.png

2.95 KB | W: | H:

doc/user/project/issues/img/confidential_issues_system_notes.png

2.28 KB | W: | H:

doc/user/project/issues/img/confidential_issues_system_notes.png
doc/user/project/issues/img/confidential_issues_system_notes.png
doc/user/project/issues/img/confidential_issues_system_notes.png
doc/user/project/issues/img/confidential_issues_system_notes.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/due_dates_create.png

7.52 KB | W: | H:

doc/user/project/issues/img/due_dates_create.png

6.83 KB | W: | H:

doc/user/project/issues/img/due_dates_create.png
doc/user/project/issues/img/due_dates_create.png
doc/user/project/issues/img/due_dates_create.png
doc/user/project/issues/img/due_dates_create.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/due_dates_edit_sidebar.png

2.37 KB | W: | H:

doc/user/project/issues/img/due_dates_edit_sidebar.png

1.66 KB | W: | H:

doc/user/project/issues/img/due_dates_edit_sidebar.png
doc/user/project/issues/img/due_dates_edit_sidebar.png
doc/user/project/issues/img/due_dates_edit_sidebar.png
doc/user/project/issues/img/due_dates_edit_sidebar.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/due_dates_issues_index_page.png

20.9 KB | W: | H:

doc/user/project/issues/img/due_dates_issues_index_page.png

18.8 KB | W: | H:

doc/user/project/issues/img/due_dates_issues_index_page.png
doc/user/project/issues/img/due_dates_issues_index_page.png
doc/user/project/issues/img/due_dates_issues_index_page.png
doc/user/project/issues/img/due_dates_issues_index_page.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/issues/img/due_dates_todos.png

5.51 KB | W: | H:

doc/user/project/issues/img/due_dates_todos.png

4.69 KB | W: | H:

doc/user/project/issues/img/due_dates_todos.png
doc/user/project/issues/img/due_dates_todos.png
doc/user/project/issues/img/due_dates_todos.png
doc/user/project/issues/img/due_dates_todos.png
  • 2-up
  • Swipe
  • Onion skin
# GitLab Issues Documentation
The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process
of solving a problem.
It allows you, your team, and your collaborators to share
and discuss proposals, before and while implementing them.
Issues and the GitLab Issue Tracker are available in all
[GitLab Products](https://about.gitlab.com/products/) as
part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
## Use-Cases
Issues can have endless applications. Just to exemplify, these are
some cases for which creating issues are most used:
- Discussing the implementation of a new idea
- Submitting feature proposals
- Asking questions
- Reporting bugs and malfunction
- Obtaining support
- Elaborating new code implementations
See also the blog post [Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/).
## Issue Tracker
The issue tracker is the collection of opened and closed issues created in a project.
![Issue tracker](img/issue_tracker.png)
Find the issue tracker by navigating to your **Project's Dashboard** > **Issues**.
## GitLab Issues Functionalities
The image bellow illustrates how an issue looks like:
![Issue view](img/issues_main_view.png)
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
## New Issue
Read through the [documentation on creating issues](create_new_issue.md).
## Closing issues
Read through the distinct ways to [close issues](closing_issues.md) on GitLab.
## Search for an issue
Learn how to [find an issue](../../search/index.md) by searching for and filtering them.
## Advanced features
### Confidential Issues
Whenever you want to keep the discussion presented in a
issue within your team only, you can make that
[issue confidential](confidential_issues.md). Even if your project
is public, that issue will be preserved. The browser will
respond with a 404 error whenever someone who is not a project
member with at least [Reporter level](../../permissions.md#project) tries to
access that issue's URL.
Learn more about them on the [confidential issues documentation](confidential_issues.md).
### Issue templates
Create templates for every new issue. They will be available from
the dropdown menu **Choose a template** when you create a new issue:
![issue template](img/issue_template.png)
Learn more about them on the [issue templates documentation](../../project/description_templates.md#creating-issue-templates).
### Crosslinking issues
Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
### GitLab Issue Board
The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
enhance your workflow by organizing and prioritizing issues in GitLab.
![Issue board](img/issue_board.png)
Find GitLab Issue Boards by navigating to your **Project's Dashboard** > **Issues** > **Board**.
Read through the documentation for [Issue Boards](../issue_board.md)
to find out more about this feature.
#### Multiple Issue Boards (EES/EEP)
Multiple Issue Boards enables you to create more than one Issue Board per project.
It's great for large projects with more than one team or in situations where a
repository is used to host the code of multiple products.
[Multiple Issue Boards](../issue_board.html#multiple-issue-boards)
are available only in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/).
### Issue's API
Read through the [API documentation](../../../api/issues.md).
# GitLab Issues Functionalities
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
## Issues Functionalities
The image bellow illustrates how an issue looks like:
![Issue view](img/issues_main_view_numbered.png)
You can find all the information on that issue on one screen.
### Issue screen
An issue starts with its status (open or closed), followed by its author,
and includes many other functionalities, numbered on the image above to
explain what they mean, one by one.
#### 1. New Issue, close issue, edit
- New issue: create a new issue in the same project
- Close issue: close this issue
- Edit: edit the same fields available when you create an issue.
#### 2. Todos
- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list
- Mark done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee
Whenever someone starts to work on an issue, it can be assigned
to that person. The assignee can be changed as much as needed.
The idea is that the assignee is responsible for that issue until
it's reassigned to someone else to take it from there.
> **Tip:**
if a user is not member of that project, it can only be
assigned to them if they created the issue themselves.
#### 4. Milestone
- Select a [milestone](../milestones/index.md) to attribute that issue to.
#### 5. Time Tracking (EES/EEP)
This feature is available only in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/).
- Estimate time: add an estimate time in which the issue will be implemented
- Spend: add the time spent on the implementation of that issue
> **Note:**
both estimate and spend times are set via [GitLab Slash Commands](../slash_commands.md).
Learn more on the [Time Tracking documentation](../../../workflow/time_tracking.md).
#### 6. Due date
When you work on a tight schedule, and it's important to
have a way to setup a deadline for implementations and for solving
problems. This can be facilitated by the [due date](due_dates.md)). Due dates
can be changed as many times as needed.
#### 7. Labels
Categorize issues by giving them [labels](../labels.md). They help to
organize team's workflows, once they enable you to work with the
[GitLab Issue Board](index.md#gitlab-issue-board).
Group Labels, which allow you to use the same labels per
group of projects, can be also given to issues. They work exactly the same,
but they are immediately available to all projects in the group.
> **Tip:**
if the label doesn't exist yet, when you click **Edit**, it opens a dropdown menu from which you can select **Create new label**.
#### 8. Weight (EES/EEP)
Issue Weights are only available in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/).
- Attribute a weight (in a 0 to 9 range) to that issue. Easy to complete
should weight 1 and very hard to complete should weight 9.
Learn more on the [Issue Weight documentation](../../../workflow/issue_weight.md).
#### 9. Participants
- People involved in that issue (mentioned in the description or in the [discussion](../../discussions/index.md)).
#### 10. Notifications
- Subscribe: if you are not a participant of the discussion on that issue, but
want to receive notifications on each new input, subscribe to it.
- Unsubscribe: if you are receiving notifications on that issue but no
longer want to receive them, unsubscribe to it.
Read more on the [notifications documentation](../../../workflow/notifications.md#issue-merge-request-events).
#### 11. Reference
- A quick "copy to clipboard" button to that issue's reference, `foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar`
is the `project-name`, and `xxx` is the issue number.
#### 12. Title and description
- Title: a plain text title describing the issue's subject.
- Description: a text field which fully supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
#### 13. @mentions
- Mentions: you can either `@mention` a user or a group present in your
GitLab instance and they will be notified via todos and email, unless that
person has disabled all notifications in their profile settings.
To change your [notification settings](../../../workflow/notifications.md) navigate to
**Profile Settings** > **Notifications** > **Global notification level**
and choose your preferences from the dropdown menu.
> **Tip:**
Avoid mentioning `@all` in issues and merge requests,
as it sends an email notification
to all the members of that project's group, which can be
interpreted as spam.
#### 14. Related Merge Requests
- Any merge requests mentioned in that issue's description
or in the issue thread.
#### 15. Award emoji
- Award an emoji to that issue.
> **Tip:**
Posting "+1" as comments in threads spam all
participants of that issue. Awarding an emoji is a way to let them
know you like it without spamming them.
#### 16. Thread
- Comments: collaborate to that issue by posting comments in its thread.
These text fields also fully support
[GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
#### 17. Comment, start a discusion, or comment and close
Once you wrote your comment, you can either:
- Click "Comment" and your comment will be published.
- Click "Start discussion": start a thread within that issue's thread to discuss specific points.
- Click "Comment and close issue": post your comment and close that issue in one click.
#### 18. New branch
- [New branch](../repository/web_editor.md#create-a-new-branch-from-an-issue):
create a new branch, followed by a new merge request which will automatically close that
issue as soon as that merge request is merged.
......@@ -4,6 +4,6 @@ class GroupUrlConstrainer
return false unless DynamicPathValidator.valid?(id)
Group.find_by_full_path(id).present?
Group.find_by_full_path(id, follow_redirects: request.get?).present?
end
end
......@@ -6,6 +6,6 @@ class ProjectUrlConstrainer
return false unless DynamicPathValidator.valid?(full_path)
Project.find_by_full_path(full_path).present?
Project.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
end
class UserUrlConstrainer
def matches?(request)
User.find_by_username(request.params[:username]).present?
User.find_by_full_path(request.params[:username], follow_redirects: request.get?).present?
end
end
......@@ -11,4 +11,5 @@ task setup_postgresql: :environment do
AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
IndexRoutesPathForLike.new.up
IndexRedirectRoutesPathForLike.new.up
end
......@@ -106,10 +106,9 @@ describe ApplicationController do
controller.send(:route_not_found)
end
it 'does redirect to login page if not authenticated' do
it 'does redirect to login page via authenticate_user! if not authenticated' do
allow(controller).to receive(:current_user).and_return(nil)
expect(controller).to receive(:redirect_to)
expect(controller).to receive(:new_user_session_path)
expect(controller).to receive(:authenticate_user!)
controller.send(:route_not_found)
end
end
......
......@@ -49,6 +49,26 @@ describe GroupsController do
expect(assigns(:issues)).to eq [issue_2, issue_1]
end
end
context 'when requesting the canonical path with different casing' do
it 'redirects to the correct casing' do
get :issues, id: group.to_param.upcase
expect(response).to redirect_to(issues_group_path(group.to_param))
expect(controller).not_to set_flash[:notice]
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'redirects to the canonical path' do
get :issues, id: redirect_route.path
expect(response).to redirect_to(issues_group_path(group.to_param))
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe 'GET #merge_requests' do
......@@ -74,6 +94,26 @@ describe GroupsController do
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
end
context 'when requesting the canonical path with different casing' do
it 'redirects to the correct casing' do
get :merge_requests, id: group.to_param.upcase
expect(response).to redirect_to(merge_requests_group_path(group.to_param))
expect(controller).not_to set_flash[:notice]
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'redirects to the canonical path' do
get :merge_requests, id: redirect_route.path
expect(response).to redirect_to(merge_requests_group_path(group.to_param))
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe 'DELETE #destroy' do
......@@ -81,7 +121,7 @@ describe GroupsController do
it 'returns 404' do
sign_in(create(:user))
delete :destroy, id: group.path
delete :destroy, id: group.to_param
expect(response.status).to eq(404)
end
......@@ -94,15 +134,39 @@ describe GroupsController do
it 'schedules a group destroy' do
Sidekiq::Testing.fake! do
expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
expect { delete :destroy, id: group.to_param }.to change(GroupDestroyWorker.jobs, :size).by(1)
end
end
it 'redirects to the root path' do
delete :destroy, id: group.path
delete :destroy, id: group.to_param
expect(response).to redirect_to(root_path)
end
context 'when requesting the canonical path with different casing' do
it 'does not 404' do
delete :destroy, id: group.to_param.upcase
expect(response).not_to have_http_status(404)
end
it 'does not redirect to the correct casing' do
delete :destroy, id: group.to_param.upcase
expect(response).not_to redirect_to(group_path(group.to_param))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'returns not found' do
delete :destroy, id: redirect_route.path
expect(response).to have_http_status(404)
end
end
end
end
......@@ -111,7 +175,7 @@ describe GroupsController do
sign_in(user)
end
it 'updates the path succesfully' do
it 'updates the path successfully' do
post :update, id: group.to_param, group: { path: 'new_path' }
expect(response).to have_http_status(302)
......@@ -125,6 +189,30 @@ describe GroupsController do
expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path')
end
context 'when requesting the canonical path with different casing' do
it 'does not 404' do
post :update, id: group.to_param.upcase, group: { path: 'new_path' }
expect(response).not_to have_http_status(404)
end
it 'does not redirect to the correct casing' do
post :update, id: group.to_param.upcase, group: { path: 'new_path' }
expect(response).not_to redirect_to(group_path(group.to_param))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'returns not found' do
post :update, id: redirect_route.path, group: { path: 'new_path' }
expect(response).to have_http_status(404)
end
end
end
describe 'POST create' do
......
......@@ -206,6 +206,7 @@ describe ProjectsController do
expect(assigns(:project)).to eq(public_project)
expect(response).to redirect_to("/#{public_project.full_path}")
expect(controller).not_to set_flash[:notice]
end
end
end
......@@ -239,19 +240,33 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path)
end
end
context 'when requesting a redirected path' do
let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
it 'redirects to the canonical path' do
get :show, namespace_id: 'foo', id: 'bar'
expect(response).to redirect_to(public_project)
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe "#update" do
render_views
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
let(:new_path) { 'renamed_path' }
let(:project_params) { { path: new_path } }
before do
sign_in(admin)
end
it "sets the repository to the right path after a rename" do
project = create(:project, :repository)
new_path = 'renamed_path'
project_params = { path: new_path }
controller.instance_variable_set(:@project, project)
sign_in(admin)
put :update,
namespace_id: project.namespace,
......@@ -262,6 +277,34 @@ describe ProjectsController do
expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(302)
end
context 'when requesting the canonical path' do
it "is case-insensitive" do
controller.instance_variable_set(:@project, project)
put :update,
namespace_id: 'FOo',
id: 'baR',
project: project_params
expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(302)
end
end
context 'when requesting a redirected path' do
let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
it 'returns not found' do
put :update,
namespace_id: 'foo',
id: 'bar',
project: project_params
expect(response).to have_http_status(404)
end
end
end
describe "#destroy" do
......@@ -297,6 +340,31 @@ describe ProjectsController do
expect(merge_request.reload.state).to eq('closed')
end
end
context 'when requesting the canonical path' do
it "is case-insensitive" do
controller.instance_variable_set(:@project, project)
sign_in(admin)
orig_id = project.id
delete :destroy, namespace_id: project.namespace, id: project.path.upcase
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
context 'when requesting a redirected path' do
let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
it 'returns not found' do
sign_in(admin)
delete :destroy, namespace_id: 'foo', id: 'bar'
expect(response).to have_http_status(404)
end
end
end
describe 'PUT #new_issue_address' do
......@@ -418,6 +486,17 @@ describe ProjectsController do
expect(parsed_body["Tags"]).to include("v1.0.0")
expect(parsed_body["Commits"]).to include("123456")
end
context 'when requesting a redirected path' do
let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
it 'redirects to the canonical path' do
get :refs, namespace_id: 'foo', id: 'bar'
expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project))
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe 'GET edit' do
......
......@@ -4,15 +4,6 @@ describe UsersController do
let(:user) { create(:user) }
describe 'GET #show' do
it 'is case-insensitive' do
user = create(:user, username: 'CamelCaseUser')
sign_in(user)
get :show, username: user.username.downcase
expect(response).to be_success
end
context 'with rendered views' do
render_views
......@@ -45,9 +36,9 @@ describe UsersController do
end
context 'when logged out' do
it 'renders 404' do
it 'redirects to login page' do
get :show, username: user.username
expect(response).to have_http_status(404)
expect(response).to redirect_to new_user_session_path
end
end
......@@ -61,6 +52,58 @@ describe UsersController do
end
end
end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
before { sign_in(user) }
context 'with exactly matching casing' do
it 'responds with success' do
get :show, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :show, username: user.username.downcase
expect(response).to redirect_to(user)
expect(controller).not_to set_flash[:notice]
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :show, username: redirect_route.path
expect(response).to redirect_to(user)
expect(controller).to set_flash[:notice].to(/moved/)
end
end
context 'when a user by that username does not exist' do
context 'when logged out' do
it 'redirects to login page' do
get :show, username: 'nonexistent'
expect(response).to redirect_to new_user_session_path
end
end
context 'when logged in' do
before { sign_in(user) }
it 'renders 404' do
get :show, username: 'nonexistent'
expect(response).to have_http_status(404)
end
end
end
end
describe 'GET #calendar' do
......@@ -88,11 +131,45 @@ describe UsersController do
expect(assigns(:contributions_calendar).projects.count).to eq(2)
end
end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
before { sign_in(user) }
context 'with exactly matching casing' do
it 'responds with success' do
get :calendar, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :calendar, username: user.username.downcase
expect(response).to redirect_to(user_calendar_path(user))
expect(controller).not_to set_flash[:notice]
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :calendar, username: redirect_route.path
expect(response).to redirect_to(user_calendar_path(user))
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe 'GET #calendar_activities' do
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
let(:user) { create(:user) }
before do
allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
......@@ -110,6 +187,38 @@ describe UsersController do
get :calendar_activities, username: user.username
expect(response).to render_template('calendar_activities')
end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
context 'with exactly matching casing' do
it 'responds with success' do
get :calendar_activities, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :calendar_activities, username: user.username.downcase
expect(response).to redirect_to(user_calendar_activities_path(user))
expect(controller).not_to set_flash[:notice]
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :calendar_activities, username: redirect_route.path
expect(response).to redirect_to(user_calendar_activities_path(user))
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe 'GET #snippets' do
......@@ -132,5 +241,83 @@ describe UsersController do
expect(JSON.parse(response.body)).to have_key('html')
end
end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
context 'with exactly matching casing' do
it 'responds with success' do
get :snippets, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :snippets, username: user.username.downcase
expect(response).to redirect_to(user_snippets_path(user))
expect(controller).not_to set_flash[:notice]
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :snippets, username: redirect_route.path
expect(response).to redirect_to(user_snippets_path(user))
expect(controller).to set_flash[:notice].to(/moved/)
end
end
end
describe 'GET #exists' do
before do
sign_in(user)
end
context 'when user exists' do
it 'returns JSON indicating the user exists' do
get :exists, username: user.username
expected_json = { exists: true }.to_json
expect(response.body).to eq(expected_json)
end
context 'when the casing is different' do
let(:user) { create(:user, username: 'CamelCaseUser') }
it 'returns JSON indicating the user exists' do
get :exists, username: user.username.downcase
expected_json = { exists: true }.to_json
expect(response.body).to eq(expected_json)
end
end
end
context 'when the user does not exist' do
it 'returns JSON indicating the user does not exist' do
get :exists, username: 'foo'
expected_json = { exists: false }.to_json
expect(response.body).to eq(expected_json)
end
context 'when a user changed their username' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'returns JSON indicating a user by that username does not exist' do
get :exists, username: 'old-username'
expected_json = { exists: false }.to_json
expect(response.body).to eq(expected_json)
end
end
end
end
end
......@@ -26,6 +26,9 @@ describe 'Board with milestone', :feature, :js do
expect(find('.tokens-container')).to have_content(milestone.title)
wait_for_vue_resource
find('.card', match: :first)
expect(all('.board')[1]).to have_selector('.card', count: 1)
end
end
......@@ -48,6 +51,9 @@ describe 'Board with milestone', :feature, :js do
end
expect(find('.tokens-container')).to have_content(milestone.title)
find('.card', match: :first)
expect(all('.board')[1]).to have_selector('.card', count: 1)
end
......@@ -56,6 +62,9 @@ describe 'Board with milestone', :feature, :js do
expect(page).not_to have_css('.js-visual-token')
expect(find('.tokens-container')).not_to have_content(milestone.title)
find('.card', match: :first)
expect(page).to have_selector('.board', count: 2)
expect(all('.board')[1]).to have_selector('.card', count: 2)
end
......@@ -64,6 +73,9 @@ describe 'Board with milestone', :feature, :js do
update_board_milestone('Upcoming')
expect(find('.tokens-container')).not_to have_content(milestone.title)
find('.board', match: :first)
expect(all('.board')[1]).to have_selector('.card', count: 0)
end
......@@ -145,9 +157,10 @@ describe 'Board with milestone', :feature, :js do
click_button 'Add issues'
page.within('.add-issues-modal') do
card = find('.card', :first)
expect(page).to have_selector('.card', count: 1)
first('.card').click
card.click
click_button 'Add 1 issue'
end
......
require 'spec_helper'
feature 'Edit group settings', feature: true do
given(:user) { create(:user) }
given(:group) { create(:group, path: 'foo') }
background do
group.add_owner(user)
login_as(user)
end
describe 'when the group path is changed' do
let(:new_group_path) { 'bar' }
let(:old_group_full_path) { "/#{group.path}" }
let(:new_group_full_path) { "/#{new_group_path}" }
scenario 'the group is accessible via the new path' do
update_path(new_group_path)
visit new_group_full_path
expect(current_path).to eq(new_group_full_path)
expect(find('h1.group-title')).to have_content(new_group_path)
end
scenario 'the old group path redirects to the new path' do
update_path(new_group_path)
visit old_group_full_path
expect(current_path).to eq(new_group_full_path)
expect(find('h1.group-title')).to have_content(new_group_path)
end
context 'with a subgroup' do
given!(:subgroup) { create(:group, parent: group, path: 'subgroup') }
given(:old_subgroup_full_path) { "/#{group.path}/#{subgroup.path}" }
given(:new_subgroup_full_path) { "/#{new_group_path}/#{subgroup.path}" }
scenario 'the subgroup is accessible via the new path' do
update_path(new_group_path)
visit new_subgroup_full_path
expect(current_path).to eq(new_subgroup_full_path)
expect(find('h1.group-title')).to have_content(subgroup.path)
end
scenario 'the old subgroup path redirects to the new path' do
update_path(new_group_path)
visit old_subgroup_full_path
expect(current_path).to eq(new_subgroup_full_path)
expect(find('h1.group-title')).to have_content(subgroup.path)
end
end
context 'with a project' do
given!(:project) { create(:project, group: group, path: 'project') }
given(:old_project_full_path) { "/#{group.path}/#{project.path}" }
given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" }
before(:context) { TestEnv.clean_test_path }
after(:example) { TestEnv.clean_test_path }
scenario 'the project is accessible via the new path' do
update_path(new_group_path)
visit new_project_full_path
expect(current_path).to eq(new_project_full_path)
expect(find('h1.project-title')).to have_content(project.name)
end
scenario 'the old project path redirects to the new path' do
update_path(new_group_path)
visit old_project_full_path
expect(current_path).to eq(new_project_full_path)
expect(find('h1.project-title')).to have_content(project.name)
end
end
end
end
def update_path(new_group_path)
visit edit_group_path(group)
fill_in 'group_path', with: new_group_path
click_button 'Save group'
end
require 'rails_helper'
feature 'Profile > Account', feature: true do
given(:user) { create(:user, username: 'foo') }
before do
login_as(user)
end
describe 'Change username' do
given(:new_username) { 'bar' }
given(:new_user_path) { "/#{new_username}" }
given(:old_user_path) { "/#{user.username}" }
scenario 'the user is accessible via the new path' do
update_username(new_username)
visit new_user_path
expect(current_path).to eq(new_user_path)
expect(find('.user-info')).to have_content(new_username)
end
scenario 'the old user path redirects to the new path' do
update_username(new_username)
visit old_user_path
expect(current_path).to eq(new_user_path)
expect(find('.user-info')).to have_content(new_username)
end
context 'with a project' do
given!(:project) { create(:project, namespace: user.namespace, path: 'project') }
given(:new_project_path) { "/#{new_username}/#{project.path}" }
given(:old_project_path) { "/#{user.username}/#{project.path}" }
before(:context) { TestEnv.clean_test_path }
after(:example) { TestEnv.clean_test_path }
scenario 'the project is accessible via the new path' do
update_username(new_username)
visit new_project_path
expect(current_path).to eq(new_project_path)
expect(find('h1.project-title')).to have_content(project.name)
end
scenario 'the old project path redirects to the new path' do
update_username(new_username)
visit old_project_path
expect(current_path).to eq(new_project_path)
expect(find('h1.project-title')).to have_content(project.name)
end
end
end
end
def update_username(new_username)
allow(user.namespace).to receive(:move_dir)
visit profile_account_path
fill_in 'user_username', with: new_username
click_button 'Update username'
end
......@@ -68,20 +68,23 @@ describe 'Edit Project Settings', feature: true do
end
describe 'project features visibility pages' do
before do
@tools =
{
builds: namespace_project_pipelines_path(project.namespace, project),
issues: namespace_project_issues_path(project.namespace, project),
wiki: namespace_project_wiki_path(project.namespace, project, :home),
snippets: namespace_project_snippets_path(project.namespace, project),
merge_requests: namespace_project_merge_requests_path(project.namespace, project),
}
let(:tools) do
{
builds: namespace_project_pipelines_path(project.namespace, project),
issues: namespace_project_issues_path(project.namespace, project),
wiki: namespace_project_wiki_path(project.namespace, project, :home),
snippets: namespace_project_snippets_path(project.namespace, project),
merge_requests: namespace_project_merge_requests_path(project.namespace, project),
}
end
context 'normal user' do
before do
login_as(member)
end
it 'renders 200 if tool is enabled' do
@tools.each do |method_name, url|
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
visit url
expect(page.status_code).to eq(200)
......@@ -89,7 +92,7 @@ describe 'Edit Project Settings', feature: true do
end
it 'renders 404 if feature is disabled' do
@tools.each do |method_name, url|
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url
expect(page.status_code).to eq(404)
......@@ -99,21 +102,21 @@ describe 'Edit Project Settings', feature: true do
it 'renders 404 if feature is enabled only for team members' do
project.team.truncate
@tools.each do |method_name, url|
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(404)
end
end
it 'renders 200 if users is member of group' do
it 'renders 200 if user is member of group' do
group = create(:group)
project.group = group
project.save
group.add_owner(member)
@tools.each do |method_name, url|
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
......@@ -128,7 +131,7 @@ describe 'Edit Project Settings', feature: true do
end
it 'renders 404 if feature is disabled' do
@tools.each do |method_name, url|
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url
expect(page.status_code).to eq(404)
......@@ -138,7 +141,7 @@ describe 'Edit Project Settings', feature: true do
it 'renders 200 if feature is enabled only for team members' do
project.team.truncate
@tools.each do |method_name, url|
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
......
require 'spec_helper'
describe 'Edit Project Settings', feature: true do
include Select2Helper
let(:user) { create(:user) }
let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
let(:project) { create(:empty_project, namespace: user.namespace, path: 'gitlab', name: 'sample') }
before do
login_as(user)
project.team << [user, :master]
end
describe 'Project settings', js: true do
describe 'Project settings section', js: true do
it 'shows errors for invalid project name' do
visit edit_namespace_project_path(project.namespace, project)
fill_in 'project_name_edit', with: 'foo&bar'
click_button 'Save changes'
expect(page).to have_field 'project_name_edit', with: 'foo&bar'
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes'
end
scenario 'shows a successful notice when the project is updated' do
it 'shows a successful notice when the project is updated' do
visit edit_namespace_project_path(project.namespace, project)
fill_in 'project_name_edit', with: 'hello world'
click_button 'Save changes'
expect(page).to have_content "Project 'hello world' was successfully updated."
end
end
describe 'Rename repository' do
it 'shows errors for invalid project path/name' do
visit edit_namespace_project_path(project.namespace, project)
fill_in 'project_name', with: 'foo&bar'
fill_in 'Path', with: 'foo&bar'
describe 'Rename repository section' do
context 'with invalid characters' do
it 'shows errors for invalid project path/name' do
rename_project(project, name: 'foo&bar', path: 'foo&bar')
expect(page).to have_field 'Project name', with: 'foo&bar'
expect(page).to have_field 'Path', with: 'foo&bar'
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
end
end
click_button 'Rename project'
context 'when changing project name' do
it 'renames the repository' do
rename_project(project, name: 'bar')
expect(find('h1.title')).to have_content(project.name)
end
context 'with emojis' do
it 'shows error for invalid project name' do
rename_project(project, name: '🚀 foo bar ☁️')
expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
end
end
end
expect(page).to have_field 'Project name', with: 'foo&bar'
expect(page).to have_field 'Path', with: 'foo&bar'
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
context 'when changing project path' do
# Not using empty project because we need a repo to exist
let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') }
before(:context) { TestEnv.clean_test_path }
after(:example) { TestEnv.clean_test_path }
specify 'the project is accessible via the new path' do
rename_project(project, path: 'bar')
new_path = namespace_project_path(project.namespace, 'bar')
visit new_path
expect(current_path).to eq(new_path)
expect(find('h1.title')).to have_content(project.name)
end
specify 'the project is accessible via a redirect from the old path' do
old_path = namespace_project_path(project.namespace, project)
rename_project(project, path: 'bar')
new_path = namespace_project_path(project.namespace, 'bar')
visit old_path
expect(current_path).to eq(new_path)
expect(find('h1.title')).to have_content(project.name)
end
context 'and a new project is added with the same path' do
it 'overrides the redirect' do
old_path = namespace_project_path(project.namespace, project)
rename_project(project, path: 'bar')
new_project = create(:empty_project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
visit old_path
expect(current_path).to eq(old_path)
expect(find('h1.title')).to have_content(new_project.name)
end
end
end
end
describe 'Rename repository name with emojis' do
it 'shows error for invalid project name' do
visit edit_namespace_project_path(project.namespace, project)
fill_in 'project_name', with: '🚀 foo bar ☁️'
describe 'Transfer project section', js: true do
# Not using empty project because we need a repo to exist
let!(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') }
let!(:group) { create(:group) }
before(:context) { TestEnv.clean_test_path }
before(:example) { group.add_owner(user) }
after(:example) { TestEnv.clean_test_path }
specify 'the project is accessible via the new path' do
transfer_project(project, group)
new_path = namespace_project_path(group, project)
visit new_path
expect(current_path).to eq(new_path)
expect(find('h1.title')).to have_content(project.name)
end
click_button 'Rename project'
specify 'the project is accessible via a redirect from the old path' do
old_path = namespace_project_path(project.namespace, project)
transfer_project(project, group)
new_path = namespace_project_path(group, project)
visit old_path
expect(current_path).to eq(new_path)
expect(find('h1.title')).to have_content(project.name)
end
expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
context 'and a new project is added with the same path' do
it 'overrides the redirect' do
old_path = namespace_project_path(project.namespace, project)
transfer_project(project, group)
new_project = create(:empty_project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
visit old_path
expect(current_path).to eq(old_path)
expect(find('h1.title')).to have_content(new_project.name)
end
end
end
end
def rename_project(project, name: nil, path: nil)
visit edit_namespace_project_path(project.namespace, project)
fill_in('project_name', with: name) if name
fill_in('Path', with: path) if path
click_button('Rename project')
wait_for_edit_project_page_reload
project.reload
end
def transfer_project(project, namespace)
visit edit_namespace_project_path(project.namespace, project)
select2(namespace.id, from: '#new_namespace_id')
click_button('Transfer project')
confirm_transfer_modal
wait_for_edit_project_page_reload
project.reload
end
def confirm_transfer_modal
fill_in('confirm_name_input', with: project.path)
click_button 'Confirm'
end
def wait_for_edit_project_page_reload
expect(find('.project-edit-container')).to have_content('Rename repository')
end
......@@ -9,7 +9,7 @@ RSpec.shared_examples "protected tags > access control > CE" do
allowed_to_create_button = find(".js-allowed-to-create")
unless allowed_to_create_button.text == access_type_name
allowed_to_create_button.click
allowed_to_create_button.trigger('click')
find('.create_access_levels-container .dropdown-menu li', match: :first)
within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
end
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Signup', feature: true do
describe 'signup with no errors' do
context "when sending confirmation email" do
before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) }
before { stub_application_setting(send_user_confirmation_email: true) }
it 'creates the user account and sends a confirmation email' do
user = build(:user)
......@@ -23,7 +23,7 @@ feature 'Signup', feature: true do
end
context "when not sending confirmation email" do
before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) }
before { stub_application_setting(send_user_confirmation_email: false) }
it 'creates the user account and goes to dashboard' do
user = build(:user)
......
......@@ -29,9 +29,37 @@ describe GroupUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey }
end
context 'when the request matches a redirect route' do
context 'for a root group' do
let!(:redirect_route) { group.redirect_routes.create!(path: 'gitlabb') }
context 'and is a GET request' do
let(:request) { build_request(redirect_route.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
context 'and is NOT a GET request' do
let(:request) { build_request(redirect_route.path, 'POST') }
it { expect(subject.matches?(request)).to be_falsey }
end
end
context 'for a nested group' do
let!(:nested_group) { create(:group, path: 'nested', parent: group) }
let!(:redirect_route) { nested_group.redirect_routes.create!(path: 'gitlabb/nested') }
let(:request) { build_request(redirect_route.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
end
end
def build_request(path)
double(:request, params: { id: path })
def build_request(path, method = 'GET')
double(:request,
'get?': (method == 'GET'),
params: { id: path })
end
end
......@@ -24,9 +24,26 @@ describe ProjectUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey }
end
end
context 'when the request matches a redirect route' do
let(:old_project_path) { 'old_project_path' }
let!(:redirect_route) { project.redirect_routes.create!(path: "#{namespace.full_path}/#{old_project_path}") }
context 'and is a GET request' do
let(:request) { build_request(namespace.full_path, old_project_path) }
it { expect(subject.matches?(request)).to be_truthy }
end
context 'and is NOT a GET request' do
let(:request) { build_request(namespace.full_path, old_project_path, 'POST') }
it { expect(subject.matches?(request)).to be_falsey }
end
end
end
def build_request(namespace, project)
double(:request, params: { namespace_id: namespace, id: project })
def build_request(namespace, project, method = 'GET')
double(:request,
'get?': (method == 'GET'),
params: { namespace_id: namespace, id: project })
end
end
......@@ -15,9 +15,26 @@ describe UserUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey }
end
context 'when the request matches a redirect route' do
let(:old_project_path) { 'old_project_path' }
let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') }
context 'and is a GET request' do
let(:request) { build_request(redirect_route.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
context 'and is NOT a GET request' do
let(:request) { build_request(redirect_route.path, 'POST') }
it { expect(subject.matches?(request)).to be_falsey }
end
end
end
def build_request(username)
double(:request, params: { username: username })
def build_request(username, method = 'GET')
double(:request,
'get?': (method == 'GET'),
params: { username: username })
end
end
......@@ -248,6 +248,7 @@ project:
- path_locks
- approver_groups
- route
- redirect_routes
- statistics
- container_repositories
- uploads
......
......@@ -9,6 +9,7 @@ describe Group, 'Routable' do
describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) }
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end
describe 'Callbacks' do
......@@ -35,10 +36,53 @@ describe Group, 'Routable' do
describe '.find_by_full_path' do
let!(:nested_group) { create(:group, parent: group) }
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
context 'without any redirect routes' do
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end
context 'with redirect routes' do
let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') }
let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) }
context 'without follow_redirects option' do
context 'with the given path not matching any route' do
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end
context 'with the given path matching the canonical route' do
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
end
context 'with the given path matching a redirect route' do
it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) }
it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) }
it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) }
end
end
context 'with follow_redirects option set to true' do
context 'with the given path not matching any route' do
it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) }
end
context 'with the given path matching the canonical route' do
it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) }
end
context 'with the given path matching a redirect route' do
it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) }
end
end
end
end
describe '.where_full_path_in' do
......
require 'rails_helper'
describe RedirectRoute, models: true do
let(:group) { create(:group) }
let!(:redirect_route) { group.redirect_routes.create(path: 'gitlabb') }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path) }
end
describe '.matching_path_and_descendants' do
let!(:redirect2) { group.redirect_routes.create(path: 'gitlabb/test') }
let!(:redirect3) { group.redirect_routes.create(path: 'gitlabb/test/foo') }
let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') }
let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') }
it 'returns correct routes' do
expect(RedirectRoute.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5])
end
end
end
require 'spec_helper'
describe Route, models: true do
let!(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
let!(:route) { group.route }
let(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
let(:route) { group.route }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
end
describe 'validations' do
before { route }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path) }
end
describe 'callbacks' do
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
route.update_attributes(path: 'foo')
end
it 'calls #delete_conflicting_redirects' do
expect(route).to receive(:delete_conflicting_redirects)
route.update_attributes(path: 'foo')
end
end
context 'after create' do
it 'calls #delete_conflicting_redirects' do
route.destroy
new_route = Route.new(source: group, path: group.path)
expect(new_route).to receive(:delete_conflicting_redirects)
new_route.save!
end
end
end
describe '.inside_path' do
let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) }
let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) }
......@@ -37,7 +61,7 @@ describe Route, models: true do
context 'when route name is set' do
before { route.update_attributes(path: 'bar') }
it "updates children routes with new path" do
it 'updates children routes with new path' do
expect(described_class.exists?(path: 'bar')).to be_truthy
expect(described_class.exists?(path: 'bar/test')).to be_truthy
expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
......@@ -56,10 +80,24 @@ describe Route, models: true do
expect(route.update_attributes(path: 'bar')).to be_truthy
end
end
context 'when conflicting redirects exist' do
let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
it 'deletes the conflicting redirects' do
route.update_attributes(path: 'bar')
expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
expect(RedirectRoute.exists?(path: 'gitlab-org')).to be_truthy
end
end
end
context 'name update' do
it "updates children routes with new path" do
it 'updates children routes with new path' do
route.update_attributes(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
......@@ -77,4 +115,72 @@ describe Route, models: true do
end
end
end
describe '#create_redirect_for_old_path' do
context 'if the path changed' do
it 'creates a RedirectRoute for the old path' do
redirect_scope = route.source.redirect_routes.where(path: 'git_lab')
expect(redirect_scope.exists?).to be_falsey
route.path = 'new-path'
route.save!
expect(redirect_scope.exists?).to be_truthy
end
end
end
describe '#create_redirect' do
it 'creates a RedirectRoute with the same source' do
redirect_route = route.create_redirect('foo')
expect(redirect_route).to be_a(RedirectRoute)
expect(redirect_route).to be_persisted
expect(redirect_route.source).to eq(route.source)
expect(redirect_route.path).to eq('foo')
end
end
describe '#delete_conflicting_redirects' do
context 'when a redirect route with the same path exists' do
let!(:redirect1) { route.create_redirect(route.path) }
it 'deletes the redirect' do
route.delete_conflicting_redirects
expect(route.conflicting_redirects).to be_empty
end
context 'when redirect routes with paths descending from the route path exists' do
let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
let!(:other_redirect) { route.create_redirect("other") }
it 'deletes all redirects with paths that descend from the route path' do
route.delete_conflicting_redirects
expect(route.conflicting_redirects).to be_empty
end
end
end
end
describe '#conflicting_redirects' do
context 'when a redirect route with the same path exists' do
let!(:redirect1) { route.create_redirect(route.path) }
it 'returns the redirect route' do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
expect(route.conflicting_redirects).to match_array([redirect1])
end
context 'when redirect routes with paths descending from the route path exists' do
let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
let!(:other_redirect) { route.create_redirect("other") }
it 'returns the redirect routes' do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4])
end
end
end
end
end
......@@ -873,6 +873,65 @@ describe User, models: true do
end
end
describe '.find_by_full_path' do
let!(:user) { create(:user) }
context 'with a route matching the given path' do
let!(:route) { user.namespace.route }
it 'returns the user' do
expect(User.find_by_full_path(route.path)).to eq(user)
end
it 'is case-insensitive' do
expect(User.find_by_full_path(route.path.upcase)).to eq(user)
expect(User.find_by_full_path(route.path.downcase)).to eq(user)
end
end
context 'with a redirect route matching the given path' do
let!(:redirect_route) { user.namespace.redirect_routes.create(path: 'foo') }
context 'without the follow_redirects option' do
it 'returns nil' do
expect(User.find_by_full_path(redirect_route.path)).to eq(nil)
end
end
context 'with the follow_redirects option set to true' do
it 'returns the user' do
expect(User.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(user)
end
it 'is case-insensitive' do
expect(User.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(user)
expect(User.find_by_full_path(redirect_route.path.downcase, follow_redirects: true)).to eq(user)
end
end
end
context 'without a route or a redirect route matching the given path' do
context 'without the follow_redirects option' do
it 'returns nil' do
expect(User.find_by_full_path('unknown')).to eq(nil)
end
end
context 'with the follow_redirects option set to true' do
it 'returns nil' do
expect(User.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
end
end
end
context 'with a group route matching the given path' do
let!(:group) { create(:group, path: 'group_path') }
it 'returns nil' do
expect(User.find_by_full_path('group_path')).to eq(nil)
end
end
end
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'project routing' do
before do
allow(Project).to receive(:find_by_full_path).and_return(false)
allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true)
allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true)
end
# Shared examples for a resource inside a Project
......@@ -93,13 +93,13 @@ describe 'project routing' do
end
context 'name with dot' do
before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) }
before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) }
it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') }
end
context 'with nested group' do
before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) }
before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) }
it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment