Commit 76f2e065 authored by Sytse Sijbrandij's avatar Sytse Sijbrandij

Merge branch 'master' into install-guide-improvements

Conflicts:
	doc/install/installation.md
parents 8ccad7c3 48d37272
......@@ -24,6 +24,7 @@ v 5.3.0
- init.d: Ensure socket is removed before starting service
- Admin area: Style teams:index, group:show pages
- Own page for failed forking
- Scrum view for milestone
v 5.2.0
- Turbolinks
......
......@@ -29,7 +29,7 @@ gem 'gitlab_git', '~> 1.3.0'
gem 'gitlab-grack', '~> 1.0.1', require: 'grack'
# LDAP Auth
gem 'gitlab_omniauth-ldap', '1.0.2', require: "omniauth-ldap"
gem 'gitlab_omniauth-ldap', '1.0.3', require: "omniauth-ldap"
# Syntax highlighter
gem "gitlab-pygments.rb", '~> 0.3.2', require: 'pygments.rb'
......@@ -72,6 +72,9 @@ gem "seed-fu"
gem "redcarpet", "~> 2.2.2"
gem "github-markup", "~> 0.7.4", require: 'github/markup'
# Asciidoc to HTML
gem "asciidoctor"
# Servers
gem "puma", '~> 2.0.1'
......
......@@ -46,6 +46,7 @@ GEM
rails (~> 3.0)
addressable (2.3.4)
arel (3.0.2)
asciidoctor (0.1.3)
awesome_print (1.1.0)
backports (2.6.7)
bcrypt-ruby (3.0.1)
......@@ -168,8 +169,8 @@ GEM
github-linguist (~> 2.3.4)
gitlab-grit (~> 2.5.1)
gitlab_meta (5.0)
gitlab_omniauth-ldap (1.0.2)
net-ldap (~> 0.2.2)
gitlab_omniauth-ldap (1.0.3)
net-ldap (~> 0.3.1)
omniauth (~> 1.0)
pyu-ruby-sasl (~> 0.0.3.1)
rubyntlm (~> 0.1.1)
......@@ -265,7 +266,7 @@ GEM
multi_xml (0.5.3)
multipart-post (1.2.0)
mysql2 (0.3.11)
net-ldap (0.2.2)
net-ldap (0.3.1)
nokogiri (1.5.9)
oauth (0.4.7)
oauth2 (0.8.1)
......@@ -517,6 +518,7 @@ PLATFORMS
DEPENDENCIES
acts-as-taggable-on
annotate!
asciidoctor
awesome_print
better_errors
binding_of_caller
......@@ -544,7 +546,7 @@ DEPENDENCIES
gitlab-pygments.rb (~> 0.3.2)
gitlab_git (~> 1.3.0)
gitlab_meta (= 5.0)
gitlab_omniauth-ldap (= 1.0.2)
gitlab_omniauth-ldap (= 1.0.3)
gon
grape (~> 0.4.1)
grape-entity (~> 0.3.0)
......
$ ->
$('.milestone-issue-filter li[data-closed]').addClass('hide')
$('.milestone-issue-filter ul.nav li a').click ->
$('.milestone-issue-filter li').toggleClass('active')
$('.milestone-issue-filter li[data-closed]').toggleClass('hide')
false
$('.milestone-merge-requests-filter li[data-closed]').addClass('hide')
$('.milestone-merge-requests-filter ul.nav li a').click ->
$('.milestone-merge-requests-filter li').toggleClass('active')
$('.milestone-merge-requests-filter li[data-closed]').toggleClass('hide')
false
......@@ -57,6 +57,7 @@
border-color: #CCC;
border-bottom: 1px solid #fff;
color: #333;
font-weight: bold;
}
}
}
......
......@@ -58,6 +58,7 @@
background: #f9f9f9;
border-radius: 0;
color: #555;
margin: 0 20px;
}
.note-file-attach {
......
......@@ -92,6 +92,10 @@ ul.notes {
.note-body {
@include md-typography;
margin-left: 45px;
.highlight {
@include border-radius(4px);
}
}
.note-header {
padding-bottom: 5px;
......
......@@ -8,7 +8,7 @@ module Issues
@issues = case params[:status]
when issues_filter[:all] then @project.issues
when issues_filter[:closed] then @project.issues.closed
when issues_filter[:to_me] then @project.issues.assigned(current_user)
when issues_filter[:to_me] then @project.issues.assigned_to(current_user)
when issues_filter[:by_me] then @project.issues.authored(current_user)
else @project.issues.opened
end
......
......@@ -51,6 +51,7 @@ module Projects
if shell.import_repository(@project.path_with_namespace, @project.import_url)
# We should create satellite for imported repo
@project.satellite.create unless @project.satellite.exists?
@project.imported = true
true
else
@project.errors.add(:import_url, 'cannot clone repo')
......
......@@ -8,10 +8,6 @@ class Admin::GroupsController < Admin::ApplicationController
end
def show
@projects = Project.scoped
@projects = @projects.not_in_group(@group) if @group.projects.present?
@projects = @projects.all
@projects.reject!(&:empty_repo?)
end
def new
......
......@@ -55,8 +55,14 @@ class Admin::UsersController < Admin::ApplicationController
def create
admin = params[:user].delete("admin")
@admin_user = User.new(params[:user], as: :admin)
opts = {
force_random_password: true,
password_expires_at: Time.now
}
@admin_user = User.new(params[:user].merge(opts), as: :admin)
@admin_user.admin = (admin && admin.to_i > 0)
@admin_user.created_by_id = current_user.id
respond_to do |format|
if @admin_user.save
......
class ApplicationController < ActionController::Base
before_filter :authenticate_user!
before_filter :reject_blocked!
before_filter :check_password_expiration
before_filter :set_current_user_for_thread
before_filter :add_abilities
before_filter :dev_tools if Rails.env == 'development'
......@@ -156,4 +157,10 @@ class ApplicationController < ActionController::Base
gon.gravatar_url = request.ssl? || Gitlab.config.gitlab.https ? Gitlab.config.gravatar.ssl_url : Gitlab.config.gravatar.plain_url
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
end
def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now
redirect_to new_profile_password_path and return
end
end
end
class PasswordsController < ApplicationController
layout 'navless'
skip_before_filter :check_password_expiration
before_filter :set_user
before_filter :set_title
def new
end
def create
new_password = params[:user][:password]
new_password_confirmation = params[:user][:password_confirmation]
result = @user.update_attributes(
password: new_password,
password_confirmation: new_password_confirmation
)
if result
@user.update_attributes(password_expires_at: nil)
redirect_to root_path, notice: 'Password successfully changed'
else
render :new
end
end
private
def set_user
@user = current_user
end
def set_title
@title = "New password"
end
end
......@@ -7,8 +7,12 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_filter :authorize_admin_snippet!, only: [:destroy]
before_filter :set_title
respond_to :html
layout 'navless'
def index
@snippets = Snippet.public.fresh.non_expired.page(params[:page]).per(20)
end
......@@ -98,4 +102,8 @@ class SnippetsController < ApplicationController
def authorize_admin_snippet!
return render_404 unless can?(current_user, :admin_personal_snippet, @snippet)
end
def set_title
@title = 'Snippets'
end
end
......@@ -142,7 +142,7 @@ module ApplicationHelper
end
def user_color_scheme_class
COLOR_SCHEMES[current_user.try(:color_scheme_id)]
COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user)
end
# Define whenever show last push event
......
......@@ -48,4 +48,36 @@ module ProjectsHelper
def remove_project_message(project)
"You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?"
end
def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
def project_nav_tab?(name)
project_nav_tabs.include? name
end
private
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
if project.repo_exists? && can?(current_user, :download_code, project)
nav_tabs << [:files, :commits, :network, :graphs]
end
if project.repo_exists? && project.merge_requests_enabled
nav_tabs << :merge_requests
end
if can?(current_user, :admin_project, project)
nav_tabs << :settings
end
[:issues, :wiki, :wall, :snippets].each do |feature|
nav_tabs << feature if project.send :"#{feature}_enabled"
end
nav_tabs.flatten
end
end
......@@ -22,8 +22,10 @@ module Issuable
scope :closed, -> { with_state(:closed) }
scope :of_group, ->(group) { where(project_id: group.project_ids) }
scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) }
scope :assigned, ->(u) { where(assignee_id: u.id)}
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :recent, -> { order("created_at DESC") }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
delegate :name,
:email,
......
......@@ -27,7 +27,7 @@ class Issue < ActiveRecord::Base
scope :cared, ->(user) { where(assignee_id: user) }
scope :authored, ->(user) { where(author_id: user) }
scope :open_for, ->(user) { opened.assigned(user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
state_machine :state, initial: :opened do
event :close do
......
......@@ -91,6 +91,15 @@ class MergeRequest < ActiveRecord::Base
if target_branch == source_branch
errors.add :branch_conflict, "You can not use same branch for source and target branches"
end
if opened? || reopened?
similar_mrs = self.project.merge_requests.where(source_branch: source_branch, target_branch: target_branch).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
errors.add :base, "There is already an open merge request for this branches"
end
end
end
def reload_code
......
......@@ -27,6 +27,7 @@ class Namespace < ActiveRecord::Base
message: "only letters, digits, spaces & '_' '-' '.' allowed." }
validates :description, length: { within: 0..255 }
validates :path, uniqueness: true, presence: true, length: { within: 1..255 },
exclusion: { in: Gitlab::Blacklist.path },
format: { with: Gitlab::Regex.path_regex,
message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
......
......@@ -79,6 +79,7 @@ class Project < ActiveRecord::Base
format: { with: Gitlab::Regex.project_name_regex,
message: "only letters, digits, spaces & '_' '-' '.' allowed. Letter should be first" }
validates :path, presence: true, length: { within: 0..255 },
exclusion: { in: Gitlab::Blacklist.path },
format: { with: Gitlab::Regex.path_regex,
message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
validates :issues_enabled, :wall_enabled, :merge_requests_enabled,
......@@ -92,7 +93,7 @@ class Project < ActiveRecord::Base
format: { with: URI::regexp(%w(http https)), message: "should be a valid url" },
if: :import?
validate :check_limit, :repo_name
validate :check_limit
# Scopes
scope :without_user, ->(user) { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) }
......@@ -166,14 +167,6 @@ class Project < ActiveRecord::Base
errors[:base] << ("Can't check your ability to create project")
end
def repo_name
denied_paths = %w(admin dashboard groups help profile projects search)
if denied_paths.include?(path)
errors.add(:path, "like #{path} is not allowed")
end
end
def to_param
if namespace
namespace.path + "/" + path
......@@ -420,6 +413,10 @@ class Project < ActiveRecord::Base
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
def imported?
imported
end
def rename_repo
old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path)
......
......@@ -42,8 +42,11 @@ class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, as: [:default, :admin]
attr_accessible :projects_limit, :can_create_team, :can_create_group, as: :admin
:extern_uid, :provider, :password_expires_at,
as: [:default, :admin]
attr_accessible :projects_limit, :can_create_team, :can_create_group,
as: :admin
attr_accessor :force_random_password
......@@ -104,6 +107,7 @@ class User < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider}
validates :projects_limit, presence: true, numericality: {greater_than_or_equal_to: 0}
validates :username, presence: true, uniqueness: true,
exclusion: { in: Gitlab::Blacklist.path },
format: { with: Gitlab::Regex.username_regex,
message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
......@@ -363,4 +367,8 @@ class User < ActiveRecord::Base
def accessible_deploy_keys
DeployKey.in_projects(self.master_projects).uniq
end
def created_by
User.find_by_id(created_by_id) if created_by_id
end
end
class ProjectObserver < BaseObserver
def after_create(project)
unless project.forked?
return true if project.forked? || project.imported?
GitlabShellWorker.perform_async(
:add_repository,
project.path_with_namespace
......@@ -8,7 +9,6 @@ class ProjectObserver < BaseObserver
log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"")
end
end
def after_update(project)
project.send_move_instructions if project.namespace_id_changed?
......
%h3.page_title
Team: #{@team.name}
%fieldset
%legend Members (#{@team.members.count})
= form_tag admin_team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do
%table#members_list
%thead
%tr
%th User name
%th Default project access
%th Team access
%th
- @team.members.each do |member|
%tr.member
%td
= link_to [:admin, member] do
= member.name
%small= "(#{member.email})"
%td= @team.human_default_projects_access(member)
%td= @team.admin?(member) ? "Admin" : "Member"
%td
%tr
%td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_username), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5'
%td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" }
%td
%span= check_box_tag :group_admin
%span Admin?
%td= submit_tag 'Add', class: "btn btn-primary", id: :add_members_to_team
New members for
= link_to @team.name, admin_team_path(@team)
team
%hr
= form_tag admin_team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do
- if @team.errors.any?
.alert.alert-error
%span= @team.errors.full_messages.first
.clearfix
= label_tag :user_ids do
Users to add
.input
= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_username), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5'
.clearfix.group-description-holder
= label_tag :default_project_access do
Default permission in projects
.input
= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" }
.clearfix
= label_tag :group_admin do
Is team admin
.input
= check_box_tag :group_admin
.clearfix.form-actions
= submit_tag 'Add users into team', class: "btn btn-primary", id: :add_members_to_team
= link_to 'Cancel', :back, class: "btn"
......@@ -24,6 +24,17 @@
= f.text_field :email, required: true, autocomplete: "off"
%span.help-inline * required
- if @admin_user.new_record?
%fieldset
%legend Password
.clearfix
= f.label :password
.input
%strong
A temporary password will be generated and sent to user.
%br
User will be forced to change it after first sign in
- else
%fieldset
%legend Password
.clearfix
......@@ -32,11 +43,6 @@
.clearfix
= f.label :password_confirmation
.input= f.password_field :password_confirmation, disabled: f.object.force_random_password
-if f.object.new_record?
.clearfix
= f.label :force_random_password do
%span Generate random password
.input= f.check_box :force_random_password, {}, true, nil
%fieldset
%legend Access
......
.row
.span6
%h3.page_title
= image_tag gravatar_icon(@admin_user.email, 90), class: "avatar s90"
%h3.page_title
User:
= @admin_user.name
- if @admin_user.blocked?
%span.cred (Blocked)
- if @admin_user.admin
%span.cred (Admin)
.pull-right
= link_to edit_admin_user_path(@admin_user), class: "btn pull-right" do
= link_to edit_admin_user_path(@admin_user), class: "btn grouped btn-small" do
%i.icon-edit
Edit
%br
%small @#{@admin_user.username}
%br
%small member since #{@admin_user.created_at.stamp("Nov 12, 2031")}
.clearfix
%hr
%p
%span.btn.btn-small
%i.icon-envelope
= mail_to @admin_user.email
- unless @admin_user == current_user
- if @admin_user.blocked?
= link_to 'Unblock', unblock_admin_user_path(@admin_user), method: :put, class: "btn btn-small success"
= link_to 'Unblock', unblock_admin_user_path(@admin_user), method: :put, class: "btn grouped btn-small success"
- else
= link_to 'Block', block_admin_user_path(@admin_user), confirm: 'USER WILL BE BLOCKED! Are you sure?', method: :put, class: "btn btn-small btn-remove"
= link_to 'Destroy', [:admin, @admin_user], confirm: "USER #{@admin_user.name} WILL BE REMOVED! Are you sure?", method: :delete, class: "btn btn-small btn-remove"
= link_to 'Block', block_admin_user_path(@admin_user), confirm: 'USER WILL BE BLOCKED! Are you sure?', method: :put, class: "btn grouped btn-small btn-remove"
= link_to 'Destroy', [:admin, @admin_user], confirm: "USER #{@admin_user.name} WILL BE REMOVED! Are you sure?", method: :delete, class: "btn grouped btn-small btn-remove"
%hr
.row
.span6
.ui-box
%h5.title
Account:
.pull-right
= image_tag gravatar_icon(@admin_user.email, 32), class: "avatar s32"
%ul.well-list
%li
%span.light Name:
%strong= @admin_user.name
%li
%span.light Username:
%strong
= @admin_user.username
%li
%span.light Email:
%strong
= mail_to @admin_user.email
%li
%span.light Member since:
%strong
= @admin_user.created_at.stamp("Nov 12, 2031")
%li
%span.light Last sign-in at:
%strong
- if @admin_user.last_sign_in_at
= @admin_user.last_sign_in_at.stamp("Nov 12, 2031")
- else
never
- if @admin_user.ldap_user?
%li
%span.light LDAP uid:
%strong
= @admin_user.extern_uid
- if @admin_user.created_by
%li
%span.light Created by:
%strong
= link_to @admin_user.created_by.name, [:admin, @admin_user.created_by]
%hr
%h5
Add User to Projects
......@@ -67,11 +103,11 @@
.span6
= render 'users/profile', user: @admin_user
.ui-box
%h5.title Projects (#{@projects.count})
%ul.well-list
- @projects.sort_by(&:name_with_namespace).each do |project|
- tm = project.team.get_tm(@admin_user.id)
%li
= link_to admin_project_path(project), class: dom_class(project) do
- if project.namespace
......@@ -79,16 +115,17 @@
\/
%strong.well-title
= truncate(project.name, length: 45)
%span.pull-right.light
- if project.owner == @admin_user
%i.icon-wrench
- tm = project.team.get_tm(@admin_user.id)
%span.label.label-info owner
- if tm
= tm.project_access_human
= link_to edit_admin_project_member_path(project, tm.user), class: "btn btn-small" do
.pull-right
= link_to edit_admin_project_member_path(project, tm.user), class: "btn grouped btn-small" do
%i.icon-edit
= link_to admin_project_member_path(project, tm.user), confirm: remove_from_project_team_message(project, @admin_user), method: :delete, class: "btn btn-small btn-remove" do
= link_to admin_project_member_path(project, tm.user), confirm: remove_from_project_team_message(project, @admin_user), method: :delete, class: "btn grouped btn-small btn-remove" do
%i.icon-remove
%p.light
%i.icon-wrench
&ndash; user is a project owner
.pull-right.light
= tm.project_access_human
&nbsp;
- if enabled_oauth_providers.present?
- providers = (enabled_oauth_providers - [:ldap])
- if providers.present?
%hr
%div{:'data-no-turbolink' => 'data-no-turbolink'}
%span Sign in with: &nbsp;
- (enabled_oauth_providers - [:ldap]).each do |provider|
- providers.each do |provider|
%span
- if default_providers.include?(provider)
= link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider)
......
......@@ -23,7 +23,7 @@
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: :file_content
.commit-button-annotation
= button_tag "Commit", class: 'btn commit-btn js-commit-button'
= button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-primary'
.message
to branch
%strong= @ref
......
......@@ -3,43 +3,48 @@
= link_to project_path(@project), title: "Project" do
%i.icon-home
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame)) do
= link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref)
- if project_nav_tab? :commits
= nav_link(controller: %w(commit commits compare repositories protected_branches)) do
= link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref)
- if project_nav_tab? :network
= nav_link(controller: %w(network)) do
= link_to "Network", project_network_path(@project, @ref || @repository.root_ref)
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
= link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref)
- if @project.issues_enabled
- if project_nav_tab? :issues
= nav_link(controller: %w(issues milestones labels)) do
= link_to url_for_project_issues do
Issues
- if @project.used_default_issues_tracker?
%span.count.issue_counter= @project.issues.opened.count
- if @project.repo_exists? && @project.merge_requests_enabled
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
= link_to project_merge_requests_path(@project) do
Merge Requests
%span.count.merge_counter= @project.merge_requests.opened.count
- if @project.wiki_enabled
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to 'Wiki', project_wiki_path(@project, :home)
- if @project.wall_enabled
- if project_nav_tab? :wall
= nav_link(controller: :walls) do
= link_to 'Wall', project_wall_path(@project)
- if @project.snippets_enabled
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to 'Snippets', project_snippets_path(@project)
- if can? current_user, :admin_project, @project
- if project_nav_tab? :settings
= nav_link(html_options: {class: "#{project_tab_class}"}) do
= link_to edit_project_path(@project), class: "stat-tab tab " do
Settings
......@@ -18,7 +18,7 @@
Members
%span.count= @team.members.count
- if can? current_user, :admin_user_team, @team
- if can? current_user, :manage_user_team, @team
= nav_link(path: 'teams#edit') do
= link_to edit_team_path(@team), class: "stat-tab tab " do
Settings
......
!!! 5
%html{ lang: "en"}
= render "layouts/head", title: "Snipepts"
%body{class: "#{app_theme} application", :'data-page' => body_data_page}
= render "layouts/head_panel", title: "Snippets"
= render "layouts/flash"
%nav.main-nav
.container
%ul
= nav_link(path: 'snippets#user_index', html_options: {class: 'home'}) do
= link_to user_snippets_path(current_user), title: "My Snippets" do
%i.icon-home
= nav_link(path: 'snippets#new') do
= link_to new_snippet_path do
New snippet
= nav_link(path: 'snippets#index') do
= link_to snippets_path do
Discover snippets
.container
.content= yield
.ui-box
%h5.title= title
%ul.well-list
- issues.each do |issue|
%li
= link_to [@project, issue] do
%span.badge{class: issue.closed? ? 'badge-important' : 'badge-info'} ##{issue.id}
= link_to_gfm truncate(issue.title, length: 60), [@project, issue]
- if issue.assignee
.pull-right
= image_tag gravatar_icon(issue.assignee.email, 16), class: "avatar s16"
%li
= link_to [@project, merge_request] do
%span.badge.badge-info ##{merge_request.id}
&ndash;
= link_to_gfm truncate(merge_request.title, length: 60), [@project, merge_request]
......@@ -55,39 +55,52 @@
= markdown @milestone.description
.row
%ul.nav.nav-tabs
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @issues.count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @merge_requests.count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @users.count
.tab-content
.tab-pane.active#tab-issues
.row
.span4
= render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned)
.span4
= render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned)
.span4
= render('issues', title: 'Completed Issues (closed)', issues: @issues.closed)
.tab-pane#tab-merge-requests
.row
.span6
.ui-box.milestone-issue-filter
.title
%ul.nav.nav-pills
%li.active= link_to('Open Issues', '#')
%li=link_to('All Issues', '#')
.ui-box
%h5.title Open
%ul.well-list
- @issues.each do |issue|
%li{data: {closed: issue.closed?}}
= link_to [@project, issue] do
%span.badge.badge-info ##{issue.id}
&ndash;
= link_to_gfm truncate(issue.title, length: 60), [@project, issue]
- @merge_requests.opened.each do |merge_request|
= render 'merge_request', merge_request: merge_request
.span6
.ui-box.milestone-merge-requests-filter
.title
%ul.nav.nav-pills
%li.active= link_to('Open Merge Requests', '#')
%li=link_to('All Merge Requests', '#')
.ui-box
%h5.title Closed
%ul.well-list
- @merge_requests.each do |merge_request|
%li{data: {closed: merge_request.closed? || merge_request.merged?}}
= link_to [@project, merge_request] do
%span.badge.badge-info ##{merge_request.id}
&ndash;
= link_to_gfm truncate(merge_request.title, length: 60), [@project, merge_request]
- @merge_requests.closed.each do |merge_request|
= render 'merge_request', merge_request: merge_request
%hr
%h6 Participants:
%div
.tab-pane#tab-participants
%ul.bordered-list
- @users.each do |user|
= link_to_member(@project, user)
.clearfix
%li
= link_to user, title: user.name, class: "dark" do
= image_tag gravatar_icon(user.email, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
......@@ -8,13 +8,14 @@
%p
login..........................................
%code= @user['email']
%p
- unless Gitlab.config.gitlab.signup_enabled
- if @user.created_by_id
%p
password..................................
%code= @password
%p
Please change your password immediately after login.
%p
You will be forced to change this password immediately after login.
%p
= link_to "Click here to login", root_url
......@@ -3,10 +3,11 @@ Hi <%= @user.name %>!
The Administrator created an account for you. Now you are a member of company GitLab application.
login.................. <%= @user.email %>
<% unless Gitlab.config.gitlab.signup_enabled %>
<% if @user.created_by_id %>
password............... <%= @password %>
You will be forced to change this password immediately after login.
<% end %>
Please change your password immediately after login.
Click here to login: <%= url_for(root_url) %>
= form_for @user, url: profile_password_path, method: :post do |f|
.light-well.padded
%p.slead
Please set new password before proceed.
%br
After successful password update you will be redirected to login screen
-if @user.errors.any?
.alert.alert-error
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
.clearfix
= f.label :password
.input= f.password_field :password, required: true
.clearfix
= f.label :password_confirmation
.input
= f.password_field :password_confirmation, required: true
.clearfix
.input
= f.submit 'Set new password', class: "btn btn-create"
......@@ -5,8 +5,8 @@
Edit
= nav_link(controller: [:team_members, :teams]) do
= link_to project_team_index_path(@project), class: "team-tab tab" do
%i.icon-user
Team
%i.icon-group
Project Members
= nav_link(controller: :deploy_keys) do
= link_to project_deploy_keys_path(@project) do
%span
......@@ -14,7 +14,7 @@
= nav_link(controller: :hooks) do
= link_to project_hooks_path(@project) do
%span
Hooks
Web Hooks
= nav_link(controller: :services) do
= link_to project_services_path(@project) do
%span
......
%h3.page_title
My Snippets
%small share code pastes with others out of git repository
= link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do
.pull-right
= link_to new_snippet_path, class: "btn btn-small add_new grouped btn-primary", title: "New Snippet" do
Add new snippet
= link_to snippets_path, class: "btn btn-small grouped" do
Discover snippets
%hr
......
%h3.page_title
Public snippets
%small share code pastes with others out of git repository
= link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do
.pull-right
= link_to new_snippet_path, class: "btn btn-small add_new grouped btn-primary", title: "New Snippet" do
Add new snippet
= link_to user_snippets_path(current_user), class: "btn btn-small grouped" do
My snippets
%hr
.row
......
= render "projects/settings_nav"
%h3.page_title
Team Members
Project Members
(#{@project.users.count})
%small
Read more about project permissions
......
......@@ -5,6 +5,7 @@
= link_to 'Projects', '#tab-projects', 'data-toggle' => 'tab'
%li
= link_to 'Edit Team', '#tab-edit', 'data-toggle' => 'tab'
- if can? current_user, :admin_user_team, @team
%li
= link_to 'Remove', '#tab-remove', 'data-toggle' => 'tab'
......
......@@ -52,7 +52,7 @@ Gitlab::Application.configure do
# config.action_mailer.raise_delivery_errors = false
# Enable threaded mode
config.threadsafe!
config.threadsafe! unless $rails_rake_task
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation can not be found)
......
......@@ -123,6 +123,7 @@ Gitlab::Application.routes.draw do
end
resource :notifications
resource :password
end
resources :keys
......
......@@ -3,7 +3,8 @@ admin = User.create(
name: "Administrator",
username: 'root',
password: "5iveL!fe",
password_confirmation: "5iveL!fe"
password_confirmation: "5iveL!fe",
password_expires_at: Time.now
)
admin.projects_limit = 10000
......
class AddPasswordExpiresAtToUsers < ActiveRecord::Migration
def change
add_column :users, :password_expires_at, :datetime
end
end
class AddCreatedByIdToUser < ActiveRecord::Migration
def change
add_column :users, :created_by_id, :integer
end
end
class AddImprotedToProject < ActiveRecord::Migration
def change
add_column :projects, :imported, :boolean, default: false, null: false
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20130522141856) do
ActiveRecord::Schema.define(:version => 20130614132337) do
create_table "deploy_keys_projects", :force => true do |t|
t.integer "deploy_key_id", :null => false
......@@ -172,6 +172,7 @@ ActiveRecord::Schema.define(:version => 20130522141856) do
t.string "issues_tracker_id"
t.boolean "snippets_enabled", :default => true, :null => false
t.datetime "last_activity_at"
t.boolean "imported", :default => false, :null => false
end
add_index "projects", ["creator_id"], :name => "index_projects_on_owner_id"
......@@ -292,6 +293,8 @@ ActiveRecord::Schema.define(:version => 20130522141856) do
t.string "state"
t.integer "color_scheme_id", :default => 1, :null => false
t.integer "notification_level", :default => 1, :null => false
t.datetime "password_expires_at"
t.integer "created_by_id"
end
add_index "users", ["admin"], :name => "index_users_on_admin"
......
......@@ -69,15 +69,15 @@ When listing resources you can pass the following parameters:
## Contents
+ [Users](doc/api/users.md)
+ [Session](doc/api/session.md)
+ [Projects](doc/api/projects.md)
+ [Project Snippets](doc/api/project_snippets.md)
+ [Repositories](doc/api/repositories.md)
+ [Issues](doc/api/issues.md)
+ [Milestones](doc/api/milestones.md)
+ [Notes](doc/api/notes.md)
+ [Deploy Keys](doc/api/deploy_keys.md)
+ [System Hooks](doc/api/system_hooks.md)
+ [Groups](doc/api/groups.md)
+ [User Teams](doc/api/user_teams.md)
+ [Users](users.md)
+ [Session](session.md)
+ [Projects](projects.md)
+ [Project Snippets](project_snippets.md)
+ [Repositories](repositories.md)
+ [Issues](issues.md)
+ [Milestones](milestones.md)
+ [Notes](notes.md)
+ [Deploy Keys](deploy_keys.md)
+ [System Hooks](system_hooks.md)
+ [Groups](groups.md)
+ [User Teams](user_teams.md)
......@@ -148,10 +148,10 @@ To setup the MySQL/PostgreSQL database and dependencies please see [`doc/install
cd /home/git/gitlab
# Checkout to stable release
sudo -u git -H git checkout 5-2-stable
sudo -u git -H git checkout 5-3-stable
**Note:**
You can change `5-2-stable` to `master` if you want the *bleeding edge* version, but do so with caution!
You can change `5-3-stable` to `master` if you want the *bleeding edge* version, but do so with caution!
## Configure it
......@@ -352,10 +352,10 @@ GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already
These steps are fairly general and you will need to figure out the exact details from the Omniauth provider's documentation.
* Add `gem "omniauth-your-auth-provider"` to the [Gemfile](https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/Gemfile#L18)
* Add `gem "omniauth-your-auth-provider"` to the [Gemfile](https://github.com/gitlabhq/gitlabhq/blob/5-3-stable/Gemfile#L18)
* Run `sudo -u git -H bundle install` to install the new gem(s)
* Add provider specific configuration options to your `config/gitlab.yml` (you can use the [auth providers section of the example config](https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/gitlab.yml.example#L53) as a reference)
* Add icons for the new provider into the [vendor/assets/images/authbuttons](https://github.com/gitlabhq/gitlabhq/tree/5-2-stable/vendor/assets/images/authbuttons) directory (you can find some more popular ones over at https://github.com/intridea/authbuttons)
* Add provider specific configuration options to your `config/gitlab.yml` (you can use the [auth providers section of the example config](https://github.com/gitlabhq/gitlabhq/blob/5-3-stable/config/gitlab.yml.example#L53) as a reference)
* Add icons for the new provider into the [vendor/assets/images/authbuttons](https://github.com/gitlabhq/gitlabhq/tree/5-3-stable/vendor/assets/images/authbuttons) directory (you can find some more popular ones over at https://github.com/intridea/authbuttons)
* Restart GitLab
### Examples
......
# From 5.2 to 5.3
### 0. Backup
It's useful to make a backup just in case things go south:
(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab user on the database version)
```bash
cd /home/git/gitlab
sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create
```
### 1. Stop server
sudo service gitlab stop
### 2. Get latest code
```bash
cd /home/git/gitlab
sudo -u git -H git fetch
sudo -u git -H git checkout 5-3-stable
```
### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL
sudo -u git -H bundle install --without development test postgres --deployment
#PostgreSQL
sudo -u git -H bundle install --without development test mysql --deployment
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
```
### 4. Update config files
* Make `/home/git/gitlab/config/gitlab.yml` same as https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/gitlab.yml.example but with your settings.
* Make `/home/git/gitlab/config/puma.rb` same as https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/puma.rb.example but with your settings.
### 5. Update Init script
```bash
sudo rm /etc/init.d/gitlab
sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab
sudo chmod +x /etc/init.d/gitlab
```
### 6. Start application
sudo service gitlab start
sudo service nginx restart
### 7. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
To make sure you didn't miss anything run a more thorough check with:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations upgrade complete!
## Things went south? Revert to previous version (5.2)
### 1. Revert the code to the previous version
Follow the [`upgrade guide from 5.1 to 5.2`](5.1-to-5.2.md), except for the database migration
(The backup is already migrated to the previous version)
### 2. Restore from the backup:
```bash
cd /home/git/gitlab
sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:restore
```
......@@ -22,5 +22,3 @@ Feature: Project Milestones
Given the milestone has open and closed issues
And I click link "v2.2"
Then I should see 3 issues
When I click link "All Issues"
Then I should see 4 issues
......@@ -45,7 +45,7 @@ class ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Home
Given 'I click the "Team" tab' do
click_link('Team')
click_link('Project Members')
end
Given 'I click the "Attachments" tab' do
......@@ -61,7 +61,7 @@ class ProjectActiveTab < Spinach::FeatureSteps
end
Given 'I click the "Hooks" tab' do
click_link('Hooks')
click_link('Web Hooks')
end
Given 'I click the "Deploy Keys" tab' do
......@@ -73,7 +73,7 @@ class ProjectActiveTab < Spinach::FeatureSteps
end
Then 'the active sub tab should be Team' do
ensure_active_sub_tab('Team')
ensure_active_sub_tab('Project Members')
end
Then 'the active sub tab should be Attachments' do
......@@ -89,7 +89,7 @@ class ProjectActiveTab < Spinach::FeatureSteps
end
Then 'the active sub tab should be Hooks' do
ensure_active_sub_tab('Hooks')
ensure_active_sub_tab('Web Hooks')
end
Then 'the active sub tab should be Deploy Keys' do
......
......@@ -57,8 +57,8 @@ class ProjectMergeRequests < Spinach::FeatureSteps
And 'I submit new merge request "Wiki Feature"' do
fill_in "merge_request_title", with: "Wiki Feature"
select "master", from: "merge_request_source_branch"
select "stable", from: "merge_request_target_branch"
select "bootstrap", from: "merge_request_source_branch"
select "master", from: "merge_request_target_branch"
click_button "Submit merge request"
end
......
......@@ -50,12 +50,6 @@ class ProjectMilestones < Spinach::FeatureSteps
end
Then "I should see 3 issues" do
page.should have_selector('.milestone-issue-filter .well-list li', count: 4)
page.should have_selector('.milestone-issue-filter .well-list li.hide', count: 1)
end
Then "I should see 4 issues" do
page.should have_selector('.milestone-issue-filter .well-list li', count: 4)
page.should_not have_selector('.milestone-issue-filter .well-list li.hide')
page.should have_selector('#tab-issues li', count: 4)
end
end
......@@ -93,7 +93,7 @@ class Userteams < Spinach::FeatureSteps
Then 'I should see issues from this team assigned to me' do
team = UserTeam.last
team.projects.each do |project|
project.issues.assigned(current_user).each do |issue|
project.issues.assigned_to(current_user).each do |issue|
page.should have_content issue.title
end
end
......@@ -121,7 +121,7 @@ class Userteams < Spinach::FeatureSteps
team = UserTeam.last
team.projects.each do |project|
team.members.each do |member|
project.issues.assigned(member).each do |issue|
project.issues.assigned_to(member).each do |issue|
page.should have_content issue.title
end
end
......@@ -131,9 +131,7 @@ class Userteams < Spinach::FeatureSteps
Given 'project from team has merge requests assigned to me' do
team = UserTeam.last
team.projects.each do |project|
team.members.each do |member|
3.times { create(:merge_request, assignee: member, project: project) }
end
create(:merge_request, assignee: current_user, project: project)
end
end
......@@ -145,31 +143,17 @@ class Userteams < Spinach::FeatureSteps
Then 'I should see merge requests from this team assigned to me' do
team = UserTeam.last
team.projects.each do |project|
team.members.each do |member|
project.issues.assigned(member).each do |merge_request|
project.merge_requests.each do |merge_request|
page.should have_content merge_request.title
end
end
end
end
Given 'project from team has merge requests assigned to team members' do
team = UserTeam.last
team.projects.each do |project|
team.members.each do |member|
3.times { create(:merge_request, assignee: member, project: project) }
end
end
end
Then 'I should see merge requests from this team assigned to me' do
team = UserTeam.last
team.projects.each do |project|
team.members.each do |member|
project.issues.assigned(member).each do |merge_request|
page.should have_content merge_request.title
end
end
member = team.members.sample
create(:merge_request, assignee: member, project: project)
end
end
......
require_relative 'shell_env'
require 'omniauth-ldap'
require_relative 'grack_ldap'
require_relative 'grack_helpers'
module Grack
class Auth < Rack::Auth::Basic
attr_accessor :user, :project
include LDAP
include Helpers
attr_accessor :user, :project, :ref, :env
def call(env)
@env = env
......@@ -14,42 +18,52 @@ module Grack
@env['PATH_INFO'] = @request.path
@env['SCRIPT_NAME'] = ""
auth!
end
private
def auth!
return render_not_found unless project
return unauthorized unless project.public || @auth.provided?
return bad_request if @auth.provided? && !@auth.basic?
if valid?
if @auth.provided?
@env['REMOTE_USER'] = @auth.username
end
return @app.call(env)
else
unauthorized
end
end
return bad_request unless @auth.basic?
def valid?
if @auth.provided?
# Authentication with username and password
login, password = @auth.credentials
@user = authenticate(login, password)
return false unless @user
@user = authenticate_user(login, password)
if @user
Gitlab::ShellEnv.set_env(@user)
@env['REMOTE_USER'] = @auth.username
else
return unauthorized
end
else
return unauthorized unless project.public
end
if authorized_git_request?
@app.call(env)
else
unauthorized
end
end
def authorized_git_request?
# Git upload and receive
if @request.get?
validate_get_request
authorize_request(@request.params['service'])
elsif @request.post?
validate_post_request
authorize_request(File.basename(@request.path))
else
false
end
end
def authenticate(login, password)
def authenticate_user(login, password)
user = User.find_by_email(login) || User.find_by_username(login)
# If the provided login was not a known email or username
......@@ -65,34 +79,12 @@ module Grack
end
end
def ldap_auth(login, password)
# Check user against LDAP backend if user is not authenticated
# Only check with valid login and password to prevent anonymous bind results
return nil unless ldap_conf.enabled && !login.blank? && !password.blank?
ldap = OmniAuth::LDAP::Adaptor.new(ldap_conf)
ldap_user = ldap.bind_as(
filter: Net::LDAP::Filter.eq(ldap.uid, login),
size: 1,
password: password
)
User.find_by_extern_uid_and_provider(ldap_user.dn, 'ldap') if ldap_user
end
def validate_get_request
validate_request(@request.params['service'])
end
def validate_post_request
validate_request(File.basename(@request.path))
end
def validate_request(service)
if service == 'git-upload-pack'
def authorize_request(service)
case service
when 'git-upload-pack'
project.public || can?(user, :download_code, project)
elsif service == 'git-receive-pack'
action = if project.protected_branch?(current_ref)
when'git-receive-pack'
action = if project.protected_branch?(ref)
:push_code_to_protected_branches
else
:push_code
......@@ -104,49 +96,24 @@ module Grack
end
end
def can?(object, action, subject)
abilities.allowed?(object, action, subject)
def project
@project ||= project_by_path(@request.path_info)
end
def ref
@ref ||= parse_ref
end
def current_ref
if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
input = Zlib::GzipReader.new(@request.body).read
def parse_ref
input = if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
Zlib::GzipReader.new(@request.body).read
else
input = @request.body.read
@request.body.read
end
# Need to reset seek point
@request.body.rewind
/refs\/heads\/([\w\.-]+)/n.match(input.force_encoding('ascii-8bit')).to_a.last
end
def project
unless instance_variable_defined? :@project
# Find project by PATH_INFO from env
if m = /^\/([\w\.\/-]+)\.git/.match(@request.path_info).to_a
@project = Project.find_with_namespace(m.last)
end
end
return @project
end
PLAIN_TYPE = {"Content-Type" => "text/plain"}
def render_not_found
[404, PLAIN_TYPE, ["Not Found"]]
end
protected
def abilities
@abilities ||= begin
abilities = Six.new
abilities << Ability
abilities
end
end
def ldap_conf
@ldap_conf ||= Gitlab.config.ldap
end
end# Auth
end# Grack
end
module Grack
module Helpers
def project_by_path(path)
if m = /^\/([\w\.\/-]+)\.git/.match(path).to_a
path_with_namespace = m.last
path_with_namespace.gsub!(/.wiki$/, '')
Project.find_with_namespace(path_with_namespace)
end
end
def render_not_found
[404, {"Content-Type" => "text/plain"}, ["Not Found"]]
end
def can?(object, action, subject)
abilities.allowed?(object, action, subject)
end
def abilities
@abilities ||= begin
abilities = Six.new
abilities << Ability
abilities
end
end
end
end
require 'omniauth-ldap'
module Grack
module LDAP
def ldap_auth(login, password)
# Check user against LDAP backend if user is not authenticated
# Only check with valid login and password to prevent anonymous bind results
return nil unless ldap_conf.enabled && !login.blank? && !password.blank?
ldap = OmniAuth::LDAP::Adaptor.new(ldap_conf)
ldap_user = ldap.bind_as(
filter: Net::LDAP::Filter.eq(ldap.uid, login),
size: 1,
password: password
)
User.find_by_extern_uid_and_provider(ldap_user.dn, 'ldap') if ldap_user
end
def ldap_conf
@ldap_conf ||= Gitlab.config.ldap
end
end
end
module Gitlab
module Blacklist
extend self
def path
%w(admin dashboard groups help profile projects search public assets u s teams merge_requests issues users snippets )
end
end
end
......@@ -20,13 +20,10 @@ describe "Admin::Users" do
describe "GET /admin/users/new" do
before do
@password = "123ABC"
visit new_admin_user_path
fill_in "user_name", with: "Big Bang"
fill_in "user_username", with: "bang"
fill_in "user_email", with: "bigbang@mail.com"
fill_in "user_password", with: @password
fill_in "user_password_confirmation", with: @password
end
it "should create new user" do
......@@ -57,26 +54,13 @@ describe "Admin::Users" do
end
it "should send valid email to user with email & password" do
Gitlab.config.gitlab.stub(:signup_enabled).and_return(false)
User.observers.enable :user_observer do
click_button "Create user"
user = User.last
email = ActionMailer::Base.deliveries.last
email.subject.should have_content("Account was created")
email.text_part.body.should have_content(user.email)
email.text_part.body.should have_content(@password)
end
end
it "should send valid email to user with email without password when signup is enabled" do
Gitlab.config.gitlab.stub(:signup_enabled).and_return(true)
User.observers.enable :user_observer do
click_button "Create user"
user = User.last
email = ActionMailer::Base.deliveries.last
email.subject.should have_content("Account was created")
email.text_part.body.should have_content(user.email)
email.text_part.body.should_not have_content(@password)
email.text_part.body.should have_content('password')
end
end
end
......
......@@ -15,7 +15,7 @@ describe Notify do
describe 'for new users, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: 'newguy@example.com') }
let(:new_user) { create(:user, email: 'newguy@example.com', created_by_id: 1) }
subject { Notify.new_user_email(new_user.id, new_user.password) }
......@@ -32,8 +32,7 @@ describe Notify do
end
it 'contains the new user\'s password' do
Gitlab.config.gitlab.stub(:signup_enabled).and_return(false)
should have_body_text /#{new_user.password}/
should have_body_text /password/
end
it 'includes a link to the site' do
......@@ -61,8 +60,7 @@ describe Notify do
end
it 'should not contain the new user\'s password' do
Gitlab.config.gitlab.stub(:signup_enabled).and_return(true)
should_not have_body_text /#{new_user.password}/
should_not have_body_text /password/
end
it 'includes a link to the site' do
......
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