Commit a5079d21 authored by Baldinof's avatar Baldinof

Merge branch 'master' into markdown_preview_shortcut

parents a603422e d9042e8b
......@@ -12,16 +12,18 @@ cache:
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
before_script:
- source ./scripts/prepare_build.sh
- ruby -v
- which ruby
- gem install bundler --no-ri --no-rdoc
- retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log
- touch log/test.log
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
stages:
......
......@@ -3,16 +3,31 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room
- Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
v 8.5.5
- Ensure removing a project removes associated Todo entries.
- Prevent a 500 error in Todos when author was removed.
v 8.5.4
- Do not cache requests for badges (including builds badge)
......
......@@ -263,7 +263,9 @@ group :development, :test do
gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails', '~> 4.6.0'
gem 'rspec-rails', '~> 3.3.0'
gem 'rspec-retry'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
......@@ -273,7 +275,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.8.1'
gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
......
......@@ -552,7 +552,7 @@ GEM
parser (2.2.3.0)
ast (>= 1.1, < 3.0)
pg (0.18.4)
poltergeist (1.8.1)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
......@@ -679,6 +679,8 @@ GEM
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-retry (0.4.5)
rspec-core
rspec-support (3.3.0)
rubocop (0.35.1)
astrolabe (~> 1.3)
......@@ -764,6 +766,8 @@ GEM
capybara (>= 2.0.0)
railties (>= 3)
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
spring (1.6.4)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
......@@ -978,7 +982,7 @@ DEPENDENCIES
org-ruby (~> 0.9.12)
paranoia (~> 2.0)
pg (~> 0.18.2)
poltergeist (~> 1.8.1)
poltergeist (~> 1.9.0)
pry-rails
quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1)
......@@ -999,6 +1003,7 @@ DEPENDENCIES
rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
rspec-retry
rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
......@@ -1017,6 +1022,7 @@ DEPENDENCIES
six (~> 0.2.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.6.4)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0)
......
......@@ -23,7 +23,7 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
when 'projects:milestones:show'
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
......
......@@ -69,7 +69,7 @@ class @Milestone
@bindIssuesSorting()
@bindMergeRequestSorting()
@bindTabsSwitching
@bindTabsSwitching()
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
......@@ -104,7 +104,7 @@ class @Milestone
).disableSelection()
bindMergeRequestSorting: ->
bindTabsSwitching: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show')
......@@ -112,7 +112,8 @@ class @Milestone
$(previousTabClass).hide()
$(currentTabClass).removeClass('hidden')
$(currentTabClass).show()
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
......
......@@ -81,8 +81,9 @@
&::before {
content: "\f00c";
position: absolute;
left: 4px;
top: 8px;
left: 5px;
top: 50%;
margin-top: -7px;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
......@@ -94,8 +95,8 @@
}
.dropdown-header {
padding-left: 10px;
padding-right: 10px;
padding-left: 5px;
padding-right: 5px;
color: $dropdown-header-color;
font-size: 13px;
line-height: 22px;
......
......@@ -19,10 +19,11 @@ li.milestone {
width: 105px;
}
.issue-row {
.issuable-row {
.color-label {
border-radius: 2px;
padding: 3px !important;
margin-right: 7px;
}
// Issue title
......@@ -44,20 +45,15 @@ li.milestone {
}
}
.issues-sortable-list {
.issue-detail {
.issues-sortable-list, .merge_requests-sortable-list {
.issuable-detail {
display: block;
margin-top: 7px;
.issue-number{
.issuable-number {
color: rgba(0,0,0,0.44);
margin-right: 5px;
}
.color-label {
padding: 6px 10px;
margin-right: 7px;
margin-top: 10px;
}
.avatar {
float: none;
}
......
......@@ -26,5 +26,5 @@
margin-right: 10px;
font-size: $gl-font-size;
border: 1px solid;
line-height: 40px;
line-height: 32px;
}
......@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
@group = Group.find_by(path: params[:id])
@group ||= Group.find_by(path: params[:id])
end
def group_params
......
# == FilterProjects
#
# Controller concern to handle projects filtering
# * by name
# * by archived state
#
module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
projects = projects.non_archived if params[:archived].blank?
projects
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
before_action :event_filter
def index
@projects = current_user.authorized_projects.sorted_by_activity.non_archived
@projects = @projects.sort(@sort = params[:sort])
@projects = current_user.authorized_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
terms = params[:filter_projects]
if terms.present?
@projects = @projects.search(terms)
end
@projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push
respond_to do |format|
......@@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred
@projects = current_user.starred_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
terms = params[:filter_projects]
if terms.present?
@projects = @projects.search(terms)
end
@projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push
@groups = []
......
class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
def index
@projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.non_archived
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
......@@ -22,8 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
@projects = @projects.non_archived
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format|
......@@ -38,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def starred
@projects = ProjectsFinder.new.execute(current_user)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
......
class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction
include MergeRequestsAction
......@@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController
def show
@last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format|
......@@ -98,7 +100,7 @@ class GroupsController < Groups::ApplicationController
end
def load_projects
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
end
# Dont allow unauthorized access to group
......
......@@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController
@blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob)
......
......@@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
@issues = @milestone.issues
@users = @milestone.participants.uniq
@merge_requests = @milestone.merge_requests
@labels = @milestone.labels
end
def create
......
......@@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
if @blob.lfs_pointer?
send_lfs_object
else
......
......@@ -263,11 +263,9 @@ class IssuableFinder
def by_label(items)
if labels?
if filter_by_no_label?
items = items.
joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
where(label_links: { id: nil })
items = items.without_label
else
items = items.joins(:labels).where(labels: { title: label_names })
items = items.with_label(label_names)
if projects
items = items.where(labels: { project_id: projects })
......
......@@ -4,7 +4,7 @@ class SnippetsFinder
case filter
when :all then
snippets(current_user).fresh.non_expired
snippets(current_user).fresh
when :by_user then
by_user(current_user, params[:user], params[:scope])
when :by_project
......@@ -27,7 +27,7 @@ class SnippetsFinder
end
def by_user(current_user, user, scope)
snippets = user.snippets.fresh.non_expired
snippets = user.snippets.fresh
return snippets.are_public unless current_user
......@@ -48,7 +48,7 @@ class SnippetsFinder
end
def by_project(current_user, project)
snippets = project.snippets.fresh.non_expired
snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id)
......
......@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
user = User.find_by(email: user_or_email.downcase)
user = User.find_by(email: user_or_email.try(:downcase))
end
if user
......
......@@ -152,4 +152,25 @@ module BlobHelper
'application/octet-stream'
end
end
def cached_blob?
stale = stale?(etag: @blob.id) # The #stale? method sets cache headers.
# Because we are opionated we set the cache headers ourselves.
response.cache_control[:public] = @project.public?
if @ref && @commit && @ref == @commit.id
# This is a link to a commit by its commit SHA. That means that the blob
# is immutable. The only reason to invalidate the cache is if the commit
# was deleted or if the user lost access to the repository.
response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
else
# A branch or tag points at this blob. That means that the expected blob
# value may change over time.
response.cache_control[:max_age] = Blob::CACHE_TIME
end
response.etag = @blob.id
!stale
end
end
......@@ -211,4 +211,15 @@ module CommitsHelper
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
def limited_commits(commits)
if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
[
commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
]
else
[commits, 0]
end
end
end
module ExploreHelper
def explore_projects_filter_path(options={})
def filter_projects_path(options={})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
......@@ -9,15 +9,7 @@ module ExploreHelper
}
options = exist_opts.merge(options)
path = if explore_controller?
explore_projects_path
elsif current_action?(:starred)
starred_dashboard_projects_path
else
dashboard_projects_path
end
path = request.path
path << "?#{options.to_param}"
path
end
......
......@@ -50,19 +50,25 @@ module LabelsHelper
@project.labels.pluck(:title)
end
def render_colored_label(label)
def render_colored_label(label, label_suffix = '')
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="label color-label") +
%( style="background-color: #{label_color}; color: #{text_color}">) +
escape_once(label.name) + '</span>'
%(style="background-color: #{label_color}; color: #{text_color}">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
def render_colored_cross_project_label(label)
label_suffix = label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix)
end
def suggested_colors
[
'#0033CC',
......@@ -119,5 +125,6 @@ module LabelsHelper
end
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
module_function :render_colored_label, :render_colored_cross_project_label,
:text_color_for_bg, :escape_once
end
......@@ -9,6 +9,32 @@ module MilestonesHelper
end
end
def milestones_label_path(opts = {})
if @project
namespace_project_issues_path(@project.namespace, @project, opts)
elsif @group
issues_group_path(@group, opts)
else
issues_dashboard_path(opts)
end
end
def milestones_browse_issuables_path(milestone, type:)
opts = { milestone_title: milestone.title }
if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
elsif @group
polymorphic_url([type, @group], opts)
else
polymorphic_url([type, :dashboard], opts)
end
end
def milestone_issues_by_label_count(milestone, label, state:)
milestone.issues.with_label(label.title).send(state).size
end
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
......
module SnippetsHelper
def lifetime_select_options
options = [
['forever', nil],
['1 day', "#{Date.current + 1.day}"],
['1 week', "#{Date.current + 1.week}"],
['1 month', "#{Date.current + 1.month}"]
]
options_for_select(options)
end
def reliable_snippet_path(snippet)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
......
......@@ -16,6 +16,16 @@ module SortingHelper
}
end
def projects_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
}
end
def sort_title_oldest_updated
'Oldest updated'
end
......
......@@ -9,6 +9,7 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject)
when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject)
......@@ -424,6 +425,10 @@ class Ability
end
end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private
def named_abilities(name)
......
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
# This method prevents the decorated object from evaluating to "truthy" when
......
......@@ -29,12 +29,15 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
......
module Milestoneish
def closed_items_count
issues.closed.size + merge_requests.closed_and_merged.size
end
def total_items_count
issues.size + merge_requests.size
end
def complete?
total_items_count == closed_items_count
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end
def remaining_days
return 0 if !due_date || expired?
(due_date - Date.today).to_i
end
end
......@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
delegate :color, :description, to: :@first_label
def self.build_collection(labels)
labels = labels.group_by(&:title)
labels.map do |title, label|
new(title, label)
labels.map do |title, labels|
new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
end
class GlobalMilestone
include Milestoneish
attr_accessor :title, :milestones
alias_attribute :name, :title
......@@ -28,33 +30,7 @@ class GlobalMilestone
end
def projects
milestones.map { |milestone| milestone.project }
end
def issue_count
milestones.map { |milestone| milestone.issues.count }.sum
end
def merge_requests_count
milestones.map { |milestone| milestone.merge_requests.count }.sum
end
def open_items_count
milestones.map { |milestone| milestone.open_items_count }.sum
end
def closed_items_count
milestones.map { |milestone| milestone.closed_items_count }.sum
end
def total_items_count
milestones.map { |milestone| milestone.total_items_count }.sum
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
@projects ||= Project.for_milestones(milestones.map(&:id))
end
def state
......@@ -76,35 +52,20 @@ class GlobalMilestone
end
def issues
@issues ||= milestones.map(&:issues).flatten.group_by(&:state)
@issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end
def merge_requests
@merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
@merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
end
def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq
end
def opened_issues
issues.values_at("opened", "reopened").compact.flatten
end
def closed_issues
issues['closed']
end
def opened_merge_requests
merge_requests.values_at("opened", "reopened").compact.flatten
end
def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten
end
def complete?
total_items_count == closed_items_count
def labels
@labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
.sort_by!(&:title)
end
def due_date
......
......@@ -48,10 +48,15 @@ class Label < ActiveRecord::Base
'~'
end
##
# Pattern used to extract label references from text
#
# This pattern supports cross-project references.
#
def self.reference_pattern
%r{
#{reference_prefix}
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
......@@ -62,24 +67,31 @@ class Label < ActiveRecord::Base
}x
end
def self.link_reference_pattern
nil
end
##
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
# Note that its argument differs from other objects implementing Referable. If
# a non-Symbol argument is given (such as a Project), it will default to :id.
#
# Examples:
#
# Label.first.to_reference # => "~1"
# Label.first.to_reference(:name) # => "~\"bug\""
# Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
def to_reference(format = :id)
if format == :name && !name.include?('"')
%(#{self.class.reference_prefix}"#{name}")
#
def to_reference(from_project = nil, format: :id)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(from_project)
project.to_reference + reference
else
"#{self.class.reference_prefix}#{id}"
reference
end
end
......@@ -98,4 +110,16 @@ class Label < ActiveRecord::Base
def template?
template
end
private
def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
if format == :name && !name.include?('"')
%("#{name}")
else
id
end
end
end
......@@ -137,9 +137,7 @@ class MergeRequest < ActiveRecord::Base
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :opened, -> { with_states(:opened, :reopened) }
scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) }
......@@ -537,6 +535,29 @@ class MergeRequest < ActiveRecord::Base
end
end
def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha
cache = {
source_sha: source_sha,
target_sha: target_sha,
diverged_commits_count: compute_diverged_commits_count
}
Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
end
cache[:diverged_commits_count]
end
def compute_diverged_commits_count
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
end
def diverged_from_target_branch?
diverged_commits_count > 0
end
def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
end
......
......@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500
COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
......
......@@ -24,12 +24,13 @@ class Milestone < ActiveRecord::Base
include Sortable
include Referable
include StripAttribute
include Milestoneish
belongs_to :project
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :participants, through: :issues, source: :assignee
has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
......@@ -92,30 +93,6 @@ class Milestone < ActiveRecord::Base
end
end
def open_items_count
self.issues.opened.count + self.merge_requests.opened.count
end
def closed_items_count
self.issues.closed.count + self.merge_requests.closed_and_merged.count
end
def total_items_count
self.issues.count + self.merge_requests.count
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end
def remaining_days
return 0 if !due_date || expired?
(due_date - Date.today).to_i
end
def expires_at
if due_date
if due_date.past?
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......
......@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
......@@ -215,6 +216,7 @@ class Project < ActiveRecord::Base
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
state_machine :import_status, initial: :none do
event :import_start do
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes
scope :fresh, -> { order("created_at DESC") }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
end
......@@ -133,18 +133,18 @@ class Repository
rugged.branches.create(branch_name, target)
end
expire_branches_cache
after_create_branch
find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
expire_tags_cache
before_push_tag
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(user, branch_name)
expire_branches_cache
before_remove_branch
branch = find_branch(branch_name)
oldrev = branch.try(:target)
......@@ -155,12 +155,12 @@ class Repository
rugged.branches.delete(branch_name)
end
expire_branches_cache
after_remove_branch
true
end
def rm_tag(tag_name)
expire_tags_cache
before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
......@@ -183,6 +183,14 @@ class Repository
end
end
def branch_count
@branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
end
def tag_count
@tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
end
# Return repo size in megabytes
# Cached in redis
def size
......@@ -278,6 +286,16 @@ class Repository
@has_visible_content = nil
end
def expire_branch_count_cache
cache.expire(:branch_count)
@branch_count = nil
end
def expire_tag_count_cache
cache.expire(:tag_count)
@tag_count = nil
end
def rebuild_cache
cache_keys.each do |key|
cache.expire(key)
......@@ -313,9 +331,17 @@ class Repository
expire_root_ref_cache
end
# Runs code before creating a new tag.
def before_create_tag
# Runs code before pushing (= creating or removing) a tag.
def before_push_tag
expire_cache
expire_tags_cache
expire_tag_count_cache
end
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
expire_tag_count_cache
end
# Runs code after a repository has been forked/imported.
......@@ -330,12 +356,21 @@ class Repository
# Runs code after a new branch has been created.
def after_create_branch
expire_branches_cache
expire_has_visible_content_cache
expire_branch_count_cache
end
# Runs code before removing an existing branch.
def before_remove_branch
expire_branches_cache
end
# Runs code after an existing branch has been removed.
def after_remove_branch
expire_has_visible_content_cache
expire_branch_count_cache
expire_branches_cache
end
def method_missing(m, *args, &block)
......@@ -812,6 +847,12 @@ class Repository
raw_repository.ls_files(actual_ref)
end
def main_language
unless empty?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end
end
private
def cache
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
participant :author, :notes
......@@ -111,10 +108,6 @@ class Snippet < ActiveRecord::Base
nil
end
def expired?
expires_at && expires_at < Time.current
end
def visibility_level_field
visibility_level
end
......
......@@ -14,6 +14,7 @@ class GitPushService < BaseService
# 3. Recognizes cross-references from commit messages
# 4. Executes the project's web hooks
# 5. Executes the project's services
# 6. Checks if the project's main language has changed
#
def execute
@project.repository.after_push_commit(branch_name)
......@@ -42,11 +43,24 @@ class GitPushService < BaseService
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages
end
# Checks if the main language has changed in the project and if so
# it updates it accordingly
update_main_language
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
update_merge_requests
end
def update_main_language
current_language = @project.repository.main_language
unless current_language == @project.main_language
return @project.update_attributes(main_language: current_language)
end
true
end
protected
def update_merge_requests
......@@ -96,7 +110,9 @@ class GitPushService < BaseService
# a different branch.
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end
end
end
......
......@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
project.repository.before_create_tag
project.repository.before_push_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
......
......@@ -21,7 +21,9 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
end
end
end
......
......@@ -66,7 +66,7 @@ class SystemNoteService
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
references = ->(label) { label.to_reference(:id) }
references = ->(label) { label.to_reference(format: :id) }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
......
......@@ -15,7 +15,7 @@
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
= render 'explore/projects/dropdown'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do
= icon('plus')
......
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
%span.milestone-row
- project = issue.project
%strong #{project.name_with_namespace} &middot;
= link_to [project.namespace.becomes(Namespace), project, issue] do
%span.cgray ##{issue.iid}
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
= image_tag avatar_icon(issue.assignee, 16), class: "avatar s16"
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list issues-sortable-list" }
- if issues
- issues.each do |issue|
= render 'issue', issue: issue
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
%span.milestone-row
- project = merge_request.project
%strong #{project.name_with_namespace} &middot;
= link_to [project.namespace.becomes(Namespace), project, merge_request] do
%span.cgray ##{merge_request.iid}
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
= image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16"
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list merge_requests-sortable-list" }
- if merge_requests
- merge_requests.each do |merge_request|
= render 'merge_request', merge_request: merge_request
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
.row
.col-sm-6
%strong
= link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
.col-sm-6
.pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to issues_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.issue_count, 'Issue'
&middot;
= link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.merge_requests_count, 'Merge Request'
.col-sm-6
= milestone_progress_bar(milestone)
.row
.col-sm-6
.expiration
= render 'shared/milestone_expired', milestone: milestone
.projects
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= milestone.project.name_with_namespace
= render 'shared/milestones/milestone',
milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
issues_path: issues_dashboard_path(milestone_title: milestone.title),
merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
milestone: milestone,
dashboard: true
- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path
.detail-page-header
.status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- if @milestone.closed?
Closed
- else
Open
%span.identifier
Milestone #{@milestone.title}
.detail-page-description.gray-content-block.second-block
%h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line
- if @milestone.complete? && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. Navigate to the project to close the milestone.
.table-holder
%table.table
%thead
%tr
%th Project
%th Open issues
%th State
%th Due date
- @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
%td
= milestone.issues.opened.count
%td
- if milestone.closed?
Closed
- else
Open
%td
= milestone.expires_at
.context
%p.lead
Progress:
#{@milestone.closed_items_count} closed
&ndash;
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All issues in this milestone
.row.prepend-top-default
.col-md-6
= render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
= render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All merge requests in this milestone
.row.prepend-top-default
.col-md-6
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
.gray-content-block.middle-block
.oneline
All participants to this milestone
%ul.bordered-list
- @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
= render 'shared/milestones/top', milestone: @milestone
= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
......@@ -4,7 +4,10 @@
.todo-title
%span.author-name
= link_to_author todo
- if todo.author
= link_to_author(todo)
- else
(removed)
%span.todo-label
= todo_action_name(todo)
= todo_target_link(todo)
......
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_updated
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_projects_filter_path(sort: sort_value_name) do
= sort_title_name
= link_to explore_projects_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to explore_projects_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
= link_to explore_projects_filter_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
......@@ -10,11 +10,11 @@
%b.caret
%ul.dropdown-menu
%li
= link_to explore_projects_filter_path(visibility_level: nil) do
= link_to filter_projects_path(visibility_level: nil) do
Any
- Gitlab::VisibilityLevel.values.each do |level|
%li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(visibility_level: level) do
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
......@@ -30,11 +30,11 @@
%b.caret
%ul.dropdown-menu
%li
= link_to explore_projects_filter_path(tag: nil) do
= link_to filter_projects_path(tag: nil) do
Any
- @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(tag: tag.name) do
%i.fa.fa-tag
= link_to filter_projects_path(tag: tag.name) do
= icon('tag')
= tag.name
......@@ -3,9 +3,10 @@
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- if @projects.present?
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
= icon('plus')
New Project
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
= icon('plus')
New Project
= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
%span.milestone-row
- project = issue.project
%strong #{project.name} &middot;
= link_to [project.namespace.becomes(Namespace), project, issue] do
%span.cgray ##{issue.iid}
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
= image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: ''
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list issues-sortable-list" }
- if issues
- issues.each do |issue|
= render 'issue', issue: issue
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
%span.milestone-row
- project = merge_request.project
%strong #{project.name} &middot;
= link_to [project.namespace.becomes(Namespace), project, merge_request] do
%span.cgray ##{merge_request.iid}
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
= image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list merge_requests-sortable-list" }
- if merge_requests
- merge_requests.each do |merge_request|
= render 'merge_request', merge_request: merge_request
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
.row
.col-sm-6
%strong
= link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
.col-sm-6
.pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to issues_group_path(@group, milestone_title: milestone.title) do
= pluralize milestone.issue_count, 'Issue'
&middot;
= link_to merge_requests_group_path(@group, milestone_title: milestone.title) do
= pluralize milestone.merge_requests_count, 'Merge Request'
.col-sm-6
= milestone_progress_bar(milestone)
.row
.col-sm-6
%div
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= milestone.project.name
.col-sm-6
- if can?(current_user, :admin_milestones, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- else
= link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
= render 'shared/milestones/milestone',
milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title),
issues_path: issues_group_path(@group, milestone_title: milestone.title),
merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
milestone: milestone
- page_title @milestone.title, "Milestones"
= render "header_title"
.detail-page-header
.status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- if @milestone.closed?
Closed
- else
Open
%span.identifier
Milestone #{@milestone.title}
.pull-right
- if can?(current_user, :admin_milestones, @group)
- if @milestone.active?
= link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- else
= link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
.detail-page-description.gray-content-block.second-block
%h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line
- if @milestone.complete? && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close the milestone now.
.table-holder
%table.table
%thead
%tr
%th Project
%th Open issues
%th State
%th Due date
- @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
%td
= milestone.issues.opened.count
%td
- if milestone.closed?
Closed
- else
Open
%td
= milestone.expires_at
.context
%p.lead
Progress:
#{@milestone.closed_items_count} closed
&ndash;
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All issues in this milestone
.row.prepend-top-default
.col-md-6
= render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
= render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All merge requests in this milestone
.row.prepend-top-default
.col-md-6
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
.gray-content-block.middle-block
.oneline
All participants to this milestone
%ul.bordered-list
- @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
......@@ -6,4 +6,4 @@
- blob = sanitize_svg(blob)
%img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
- else
%img{src: namespace_project_raw_path(@project.namespace, @project, @id)}
%img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
$('.js-totalbranch-count').html("#{@repository.branches.size}")
$('.js-totalbranch-count').html("#{@repository.branch_count}")
- commits, hidden = limited_commits(@commits)
- commits = Commit.decorate(commits, @project)
%div.panel.panel-default
.panel-heading
Commits (#{@commits.count})
- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
- if hidden > 0
%ul.well-list
- Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit|
- commits.each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled
other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues.
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
%ul.well-list= render Commit.decorate(@commits, @project), project: @project
%ul.well-list= render commits, project: @project
- unless defined?(project)
- project = @project
- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
- commits, hidden = limited_commits(@commits)
- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
.row.commits-row
.col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date
......@@ -13,3 +15,7 @@
%ul.bordered-list
= render commits, project: project
%hr.lists-separator
- if hidden > 0
.alert.alert-warning
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
......@@ -15,9 +15,9 @@
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
Branches
%span.badge.js-totalbranch-count= @repository.branches.size
%span.badge.js-totalbranch-count= @repository.branch_count
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
%span.badge.js-totaltags-count= @repository.tags.length
%span.badge.js-totaltags-count= @repository.tag_count
......@@ -34,6 +34,8 @@
%span into
= link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
= @merge_request.target_branch
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
......
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) }
%span
= link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title
.issue-detail
= link_to [@project.namespace.becomes(Namespace), @project, issue] do
%span.issue-number ##{issue.iid}
- issue.labels.each do |label|
= render_colored_label(label)
- if issue.assignee
= image_tag avatar_icon(issue.assignee, 16), class: "avatar s24", alt: ''
.panel.panel-default
.panel-heading
= title
.pull-right= issues.size
%ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id }
- issues.sort_by(&:position).each do |issue|
= render 'issue', issue: issue
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) }
%span.str-truncated
= link_to [@project.namespace.becomes(Namespace), @project, merge_request] do
%span.cgray ##{merge_request.iid}
= link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
= image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id }
- merge_requests.sort_by(&:position).each do |merge_request|
= render 'merge_request', merge_request: merge_request
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
.row
.col-sm-6
%strong
= link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
.col-sm-6
.pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
= pluralize milestone.issues.count, 'Issue'
&middot;
= link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
= pluralize milestone.merge_requests.count, 'Merge Request'
.col-sm-6
= milestone_progress_bar(milestone)
.row
.col-sm-6
= render 'shared/milestone_expired', milestone: milestone
.col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
= icon('pencil-square-o')
Edit
\
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
= link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
= icon('trash-o')
Delete
= render 'shared/milestones/milestone',
milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone),
issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
milestone: milestone
......@@ -42,102 +42,9 @@
= preserve do
= markdown @milestone.description
- if @milestone.issues.any? && @milestone.can_be_closed?
- if @milestone.complete? && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
.context.prepend-top-default
.milestone-summary
%h4 Progress
%strong= @milestone.issues.count
issues:
%span.milestone-stat
%strong= @milestone.open_items_count
open and
%strong= @milestone.closed_items_count
closed
%span.milestone-stat
%strong== #{@milestone.percent_complete}%
complete
%span.milestone-stat
%span.remaining-days= milestone_remaining_days(@milestone)
%span.pull-right.tab-issues-buttons
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
%i.fa.fa-plus
New Issue
- if can?(current_user, :read_issue, @project)
= link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
%span.pull-right.tab-merge-requests-buttons.hidden
- if can?(current_user, :read_merge_request, @project)
= link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
= milestone_progress_bar(@milestone)
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
%span.badge= @issues.count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
Merge Requests
%span.badge= @merge_requests.count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @users.count
%li
= link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Labels
%span.badge= @labels.count
.tab-content.milestone-content
.tab-pane.active#tab-issues
.row.prepend-top-default
.col-md-4
= render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned')
.col-md-4
= render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned, id: 'ongoing')
.col-md-4
= render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed')
.tab-pane#tab-merge-requests
.row.prepend-top-default
.col-md-3
= render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned')
.col-md-3
= render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing')
.col-md-3
= render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed')
.col-md-3
.panel.panel-primary
.panel-heading Merged
%ul.well-list
- @merge_requests.merged.each do |merge_request|
= render 'merge_request', merge_request: merge_request
.tab-pane#tab-participants
%ul.bordered-list
- @users.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
.tab-pane#tab-labels
%ul.bordered-list.manage-labels-list
- @labels.each do |label|
%li
= render_colored_label(label)
- args = [@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title, label_name: label.title]
- options = args.extract_options!
%span.issues-count
= link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do
= pluralize label.open_issues_count, 'open issue'
%span.issues-count
= link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do
= pluralize label.closed_issues_count, 'closed issue'
= render 'shared/milestones/summary', milestone: @milestone, project: @project
= render 'shared/milestones/tabs', milestone: @milestone
-# @project is present when viewing Project's milestone
- project = @project || issuable.project
- assignee = issuable.assignee
- issuable_type = issuable.class.table_name
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
%span
- if show_project_name
%strong #{project.name} &middot;
- elsif show_full_project_name
%strong #{project.name_with_namespace} &middot;
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
%div{class: 'issuable-detail'}
= link_to [project.namespace.becomes(Namespace), project, issuable] do
%span{ class: 'issuable-number' }>= issuable.to_reference
- issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- render_colored_label(label)
- if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
class: 'has_tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
- show_counter = local_assigns.fetch(:show_counter, false)
- primary = local_assigns.fetch(:primary, false)
- panel_class = primary ? 'panel-primary' : 'panel-default'
.panel{ class: panel_class }
.panel-heading
= title
- if show_counter
.pull-right= issuables.size
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
= render partial: 'shared/milestones/issuable',
collection: issuables.sort_by(&:position),
as: :issuable,
locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
.row.prepend-top-default
.col-md-4
= render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
.col-md-4
= render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
.col-md-4
= render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true)
%ul.bordered-list.manage-labels-list
- labels.each do |label|
- options = { milestone_title: @milestone.title, label_name: label.title }
%li
%span.label-row
= link_to milestones_label_path(options) do
- render_colored_label(label)
%span.prepend-left-10
= markdown(label.description, pipeline: :single_line)
.pull-right
%strong.issues-count
= link_to milestones_label_path(options.merge(state: 'opened')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
%strong.issues-count
= link_to milestones_label_path(options.merge(state: 'closed')) do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
.row.prepend-top-default
.col-md-3
= render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned')
.col-md-3
= render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing')
.col-md-3
= render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed')
.col-md-3
= render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true)
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first)
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
.col-sm-6
%strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
.col-sm-6
.pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to pluralize(milestone.issues.size, 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
- if milestone.is_a?(GlobalMilestone)
.row
.col-sm-6
.expiration= render('shared/milestone_expired', milestone: milestone)
.projects
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= dashboard ? milestone.project.name_with_namespace : milestone.project.name
- if @group
.col-sm-6
- if can?(current_user, :admin_milestones, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- else
= link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
- if @project
.row
.col-sm-6= render('shared/milestone_expired', milestone: milestone)
.col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
= icon('pencil-square-o')
Edit
\
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
= link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
= icon('trash-o')
Delete
%ul.bordered-list
- users.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
- project = local_assigns[:project]
.context.prepend-top-default
.milestone-summary
%h4 Progress
%strong= milestone.issues.size
issues:
%span.milestone-stat
%strong= milestone.issues.opened.size
open and
%strong= milestone.issues.closed.size
closed
%span.milestone-stat
%strong== #{milestone.percent_complete}%
complete
%span.milestone-stat
%span.remaining-days= milestone_remaining_days(milestone)
%span.pull-right.tab-issues-buttons
- if project && can?(current_user, :create_issue, project)
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
%i.fa.fa-plus
New Issue
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
%span.pull-right.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
= milestone_progress_bar(milestone)
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
%span.badge= milestone.issues.size
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
Merge Requests
%span.badge= milestone.merge_requests.size
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= milestone.participants.count
%li
= link_to '#tab-labels', 'data-toggle' => 'tab' do
Labels
%span.badge= milestone.labels.count
- show_project_name = local_assigns.fetch(:show_project_name, false)
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
.tab-content.milestone-content
.tab-pane.active#tab-issues
= render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
= render 'shared/milestones/participants_tab', users: milestone.participants
.tab-pane#tab-labels
= render 'shared/milestones/labels_tab', labels: milestone.labels
- page_title milestone.title, "Milestones"
- group = local_assigns[:group]
.detail-page-header
.status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
- if milestone.closed?
Closed
- elsif milestone.expired?
Expired
- else
Open
%span.identifier
Milestone #{milestone.title}
- if milestone.expires_at
%span.creator
&middot;
= milestone.expires_at
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
- if milestone.active?
= link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- else
= link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
.detail-page-description.gray-content-block.second-block
%h2.title
= markdown escape_once(milestone.title), pipeline: :single_line
- if milestone.complete? && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
.table-holder
%table.table
%thead
%tr
%th Project
%th Open issues
%th State
%th Due date
- milestone.milestones.each do |ms|
%tr
%td
- project_name = group ? ms.project.name : ms.project.name_with_namespace
= link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms)
%td
= ms.issues.opened.count
%td
- if ms.closed?
Closed
- else
Open
%td
= ms.expires_at
- @sort ||= sort_value_recently_updated
- archived = params[:archived]
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- projects_sort_options_hash.each do |value, title|
%li
= link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do
= title
%li.divider
%li
= link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects
%li
= link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
Show archived projects
......@@ -28,6 +28,9 @@
= project.name
.controls
- if project.main_language
%span
= project.main_language
- if ci_commit
%span
= render_ci_status(ci_commit)
......
......@@ -7,8 +7,6 @@ Rails.application.configure do
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = false
config.cache_store = :null_store
# Configure static asset server for tests with Cache-Control for performance
config.serve_static_files = true
config.static_cache_control = "public, max-age=3600"
......
class AddMainLanguageToRepository < ActiveRecord::Migration
def change
add_column :projects, :main_language, :string
end
end
class RemoveExpiresAtFromSnippets < ActiveRecord::Migration
def change
remove_column :snippets, :expires_at, :datetime
end
end
class FixTodos < ActiveRecord::Migration
def up
execute <<-SQL
DELETE FROM todos
WHERE todos.target_type IN ('Commit', 'ProjectSnippet')
OR NOT EXISTS (
SELECT *
FROM projects
WHERE projects.id = todos.project_id
)
SQL
end
def down
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160222153918) do
ActiveRecord::Schema.define(version: 20160309140734) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -697,6 +697,7 @@ ActiveRecord::Schema.define(version: 20160222153918) do
t.integer "build_timeout", default: 3600, null: false
t.boolean "pending_delete", default: false
t.boolean "public_builds", default: true, null: false
t.string "main_language"
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
......@@ -777,7 +778,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "file_name"
t.datetime "expires_at"
t.string "type"
t.integer "visibility_level", default: 0, null: false
end
......@@ -785,7 +785,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
......
......@@ -213,8 +213,7 @@ Example response:
## Commit status
Since GitLab 8.1, this is the new commit status API. The documentation in
[ci/api/commits](../ci/api/commits.md) is deprecated.
Since GitLab 8.1, this is the new commit status API.
### Get the status of a commit
......
......@@ -145,7 +145,6 @@ Parameters:
"state": "active",
"created_at": "2013-09-30T13:46:01Z"
},
"expires_at": null,
"updated_at": "2013-10-02T07:34:20Z",
"created_at": "2013-10-02T07:34:20Z"
}
......
......@@ -51,7 +51,6 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z"
}
......
......@@ -151,6 +151,8 @@ Parameters:
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z",
"confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null,
"skype": "",
"linkedin": "",
......
# GitLab CI API
## Resources
- [Projects](projects.md)
- [Runners](runners.md)
- [Commits](commits.md)
- [Builds](builds.md)
## Authentication
GitLab CI API uses different types of authentication depends on what API you use.
Each API document has section with information about authentication you need to use.
GitLab CI API has 4 authentication methods:
* GitLab user token & GitLab url
* GitLab CI project token
* GitLab CI runners registration token
* GitLab CI runner token
### Authentication #1: GitLab user token & GitLab url
Authentication is done by
sending the `private-token` of a valid user and the `url` of an
authorized GitLab instance via a query string along with the API
request:
GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
## Purpose
If preferred, you may instead send the `private-token` as a header in
your request:
Main purpose of GitLab CI API is to provide necessary data and context for
GitLab CI Runners.
curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/"
For consumer API take a look at this [documentation](../../api/README.md) where
you will find all relevant information.
## API Prefix
### Authentication #2: GitLab CI project token
Current CI API prefix is `/ci/api/v1`.
Each project in GitLab CI has it own token.
It can be used to get project commits and builds information.
You can use project token only for certain project.
You need to prepend this prefix to all examples in this documentation, like:
### Authentication #3: GitLab CI runners registration token
GET /ci/api/v1/builds/:id/artifacts
This token is not persisted and is generated on each application start.
It can be used only for registering new runners in system. You can find it on
GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners
### Authentication #4: GitLab CI runner token
Every GitLab CI runner has it own token that allow it to receive and update
GitLab CI builds. This token exists of internal purposes and should be used only
by runners
## JSON
All API requests are serialized using JSON. You don't need to specify
`.json` at the end of API URL.
## Status codes
The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave.
API request types:
- `GET` requests access one or more resources and return the result as JSON
- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not.
The following list shows the possible return codes for API requests.
Return values:
## Resources
- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON
- `201 Created` - The `POST` request was successful and the resource is returned as JSON
- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
- `405 Method Not Allowed` - The request is not supported
- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
- `422 Unprocessable` - The entity could not be processed
- `500 Server Error` - While handling the request something went wrong on the server side
- [Builds](builds.md)
- [Runners](runners.md)
# Builds API
This API used by runners to receive and update builds.
API used by runners to receive and update builds.
__Authentication is done by runner token__
_**Note:** This API is intended to be used only by Runners as their own
communication channel. For the consumer API see the
[Builds API](../../api/builds.md)._
## Authentication
This API uses two types of authentication:
1. Unique runner's token
Token assigned to runner after it has been registered.
2. Using build authorization token
This is project's CI token that can be found in Continuous Integration
project settings.
Build authorization token can be passed as a parameter or a value of
`BUILD-TOKEN` header. This method are interchangeable.
## Builds
### Runs oldest pending build by runner
POST /ci/builds/register
POST /ci/api/v1/builds/register
Parameters:
* `token` (required) - The unique token of runner
Returns:
```json
{
"id": 48584,
"ref": "0.1.1",
"tag": true,
"sha": "d63117656af6ff57d99e50cc270f854691f335ad",
"status": "success",
"name": "pages",
"token": "9dd60b4f1a439d1765357446c1084c",
"stage": "test",
"project_id": 479,
"project_name": "test",
"commands": "echo commands",
"repo_url": "http://gitlab-ci-token:token@gitlab.example/group/test.git",
"before_sha": "0000000000000000000000000000000000000000",
"allow_git_fetch": false,
"options": {
"image": "docker:image",
"artifacts": {
"paths": [
"public"
]
},
"cache": {
"paths": [
"vendor"
]
}
},
"timeout": 3600,
"variables": [
{
"key": "CI_BUILD_TAG",
"value": "0.1.1",
"public": true
}
],
"depends_on_builds": [
{
"id": 48584,
"ref": "0.1.1",
"tag": true,
"sha": "d63117656af6ff57d99e50cc270f854691f335ad",
"status": "success",
"name": "build",
"token": "9dd60b4f1a439d1765357446c1084c",
"stage": "build",
"project_id": 479,
"project_name": "test",
"artifacts_file": {
"filename": "artifacts.zip",
"size": 0
}
}
]
}
```
* `token` (required) - Unique runner token
### Update details of an existing build
PUT /ci/builds/:id
PUT /ci/api/v1/builds/:id
Parameters:
* `id` (required) - The ID of a project
* `token` (required) - Unique runner token
* `state` (optional) - The state of a build
* `trace` (optional) - The trace of a build
### Upload artifacts to build
POST /ci/api/v1/builds/:id/artifacts
Parameters:
* `id` (required) - The ID of a build
* `token` (required) - The build authorization token
* `file` (required) - Artifacts file
### Download the artifacts file from build
GET /ci/api/v1/builds/:id/artifacts
Parameters:
* `id` (required) - The ID of a build
* `token` (required) - The build authorization token
### Remove the artifacts file from build
DELETE /ci/api/v1/builds/:id/artifacts
Parameters:
* ` id` (required) - The ID of a build
* `token` (required) - The build authorization token
# Commits API
**DEPRECATED**
Since GitLab 8.1, there is a new commit status API. Please see the [revised
documentation](../../api/commits.md#commit-status).
---
__Authentication is done by GitLab CI project token__
## Commits
### Retrieve all commits per project
Get list of commits per project
GET /ci/commits
Parameters:
* `project_id` (required) - The ID of a project
* `project_token` (requires) - Project token
* `page` (optional)
* `per_page` (optional) - items per request (default is 20)
Returns:
```json
[{
"id": 3,
"ref": "master",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"project_id": 2,
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"created_at": "2014-11-05T09:46:35.247Z",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"duration": 5.062692165374756,
"git_commit_message": "wow\n",
"git_author_name": "Administrator",
"git_author_email": "admin@example.com",
"builds": [{
"id": 7,
"project_id": 2,
"ref": "master",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"created_at": "2014-11-05T09:46:35.259Z",
"updated_at": "2014-11-05T09:46:44.255Z",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"started_at": "2014-11-05T09:46:39.192Z",
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"runner_id": 1,
"coverage": null,
"commit_id": 3
}]
}]
```
### Create commit
Inform GitLab CI about new commit you want it to build.
__If commit already exists in GitLab CI it will not be created__
POST /ci/commits
Parameters:
* `project_id` (required) - The ID of a project
* `project_token` (requires) - Project token
* `data` (required) - Push data. For example see comment in `lib/api/commits.rb`
Returns:
```json
{
"id": 3,
"ref": "master",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"project_id": 2,
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"created_at": "2014-11-05T09:46:35.247Z",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"duration": 5.062692165374756,
"git_commit_message": "wow\n",
"git_author_name": "Administrator",
"git_author_email": "admin@example.com",
"builds": [{
"id": 7,
"project_id": 2,
"ref": "master",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"created_at": "2014-11-05T09:46:35.259Z",
"updated_at": "2014-11-05T09:46:44.255Z",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"started_at": "2014-11-05T09:46:39.192Z",
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"runner_id": 1,
"coverage": null,
"commit_id": 3
}]
}
```
# Projects API
This API is intended to aid in the setup and configuration of
projects on GitLab CI.
__Authentication is done by GitLab user token & GitLab url__
## Projects
### List Authorized Projects
Lists all projects that the authenticated user has access to.
```
GET /ci/projects
```
Returns:
```json
[
{
"id" : 271,
"name" : "gitlabhq",
"timeout" : 1800,
"token" : "iPWx6WM4lhHNedGfBpPJNP",
"default_ref" : "master",
"gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
"path" : "gitlab/gitlab-shell",
"always_build" : false,
"polling_interval" : null,
"public" : false,
"ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
"gitlab_id" : 3
},
{
"id" : 272,
"name" : "gitlab-ci",
"timeout" : 1800,
"token" : "iPWx6WM4lhHNedGfBpPJNP",
"default_ref" : "master",
"gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
"path" : "gitlab/gitlab-shell",
"always_build" : false,
"polling_interval" : null,
"public" : false,
"ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
"gitlab_id" : 4
}
]
```
### List Owned Projects
Lists all projects that the authenticated user owns.
```
GET /ci/projects/owned
```
Returns:
```json
[
{
"id" : 272,
"name" : "gitlab-ci",
"timeout" : 1800,
"token" : "iPWx6WM4lhHNedGfBpPJNP",
"default_ref" : "master",
"gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
"path" : "gitlab/gitlab-shell",
"always_build" : false,
"polling_interval" : null,
"public" : false,
"ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
"gitlab_id" : 4
}
]
```
### Single Project
Returns information about a single project for which the user is
authorized.
GET /ci/projects/:id
Parameters:
* `id` (required) - The ID of the GitLab CI project
### Create Project
Creates a GitLab CI project using GitLab project details.
POST /ci/projects
Parameters:
* `name` (required) - The name of the project
* `gitlab_id` (required) - The ID of the project on the GitLab instance
* `default_ref` (optional) - The branch to run on (default to `master`)
### Update Project
Updates a GitLab CI project using GitLab project details that the
authenticated user has access to.
PUT /ci/projects/:id
Parameters:
* `name` - The name of the project
* `default_ref` - The branch to run on (default to `master`)
### Remove Project
Removes a GitLab CI project that the authenticated user has access to.
DELETE /ci/projects/:id
Parameters:
* `id` (required) - The ID of the GitLab CI project
### Link Project to Runner
Links a runner to a project so that it can make builds (only via
authorized user).
POST /ci/projects/:id/runners/:runner_id
Parameters:
* `id` (required) - The ID of the GitLab CI project
* `runner_id` (required) - The ID of the GitLab CI runner
### Remove Project from Runner
Removes a runner from a project so that it can not make builds (only
via authorized user).
DELETE /ci/projects/:id/runners/:runner_id
Parameters:
* `id` (required) - The ID of the GitLab CI project
* `runner_id` (required) - The ID of the GitLab CI runner
\ No newline at end of file
# Runners API
API used by runners to register and delete themselves.
_**Note:** This API is intended to be used only by Runners as their own
communication channel. For the consumer API see the
[new Runners API](../../api/runners.md)._
## Runners
### Retrieve all runners
## Authentication
__Authentication is done by GitLab user token & GitLab url__
This API uses two types of authentication:
Used to get information about all runners registered on the GitLab CI
instance.
1. Unique runner's token
GET /ci/runners
Token assigned to runner after it has been registered.
Returns:
2. Using runners' registration token
```json
[
{
"id" : 85,
"token" : "12b68e90394084703135"
},
{
"id" : 86,
"token" : "76bf894e969364709864"
},
]
```
This is a token that can be found in project's settings.
It can be also found in Admin area &raquo; Runners settings.
### Register a new runner
There are two types of tokens you can pass - shared runner registration
token or project specific registration token.
## Runners
__Authentication is done with a Shared runner registration token or a project Specific runner registration token__
### Register a new runner
Used to make GitLab CI aware of available runners.
POST /ci/runners/register
POST /ci/api/v1/runners/register
Parameters:
* `token` (required) - The registration token. It is 2 types of token you can pass here.
* `token` (required) - Registration token
1. Shared runner registration token
2. Project specific registration token
Returns:
```json
{
"id" : 85,
"token" : "12b68e90394084703135"
}
```
### Delete a runner
Used to remove runner.
__Authentication is done by runner token__
Used to removing runners.
DELETE /ci/runners/delete
DELETE /ci/api/v1/runners/delete
Parameters:
* `token` (required) - The runner token.
Returns:
```json
{
"id" : 1,
"token" : "d14963981a428f70121777e50643d1",
"created_at" : "2015-02-26T11:39:39.232Z",
"updated_at" : "2015-02-26T11:39:39.232Z",
"description" : "awesome runner"
}
```
* `token` (required) - Unique runner token
......@@ -9,4 +9,5 @@
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
- [SQL guidelines](sql.md) for SQL guidelines
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
# SQL Query Guidelines
This document describes various guidelines to follow when writing SQL queries,
either using ActiveRecord/Arel or raw SQL queries.
## Using LIKE Statements
The most common way to search for data is using the `LIKE` statement. For
example, to get all issues with a title starting with "WIP:" you'd write the
following query:
```sql
SELECT *
FROM issues
WHERE title LIKE 'WIP:%';
```
On PostgreSQL the `LIKE` statement is case-sensitive. On MySQL this depends on
the case-sensitivity of the collation, which is usually case-insensitive. To
perform a case-insensitive `LIKE` on PostgreSQL you have to use `ILIKE` instead.
This statement in turn isn't supported on MySQL.
To work around this problem you should write `LIKE` queries using Arel instead
of raw SQL fragments as Arel automatically uses `ILIKE` on PostgreSQL and `LIKE`
on MySQL. This means that instead of this:
```ruby
Issue.where('title LIKE ?', 'WIP:%')
```
You'd write this instead:
```ruby
Issue.where(Issue.arel_table[:title].matches('WIP:%'))
```
Here `matches` generates the correct `LIKE` / `ILIKE` statement depending on the
database being used.
If you need to chain multiple `OR` conditions you can also do this using Arel:
```ruby
table = Issue.arel_table
Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%')))
```
For PostgreSQL this produces:
```sql
SELECT *
FROM issues
WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%')
```
In turn for MySQL this produces:
```sql
SELECT *
FROM issues
WHERE (title LIKE 'WIP:%' OR foo LIKE 'WIP:%')
```
## LIKE & Indexes
Neither PostgreSQL nor MySQL use any indexes when using `LIKE` / `ILIKE` with a
wildcard at the start. For example, this will not use any indexes:
```sql
SELECT *
FROM issues
WHERE title ILIKE '%WIP:%';
```
Because the value for `ILIKE` starts with a wildcard the database is not able to
use an index as it doesn't know where to start scanning the indexes.
MySQL provides no known solution to this problem. Luckily PostgreSQL _does_
provide a solution: trigram GIN indexes. These indexes can be created as
follows:
```sql
CREATE INDEX [CONCURRENTLY] index_name_here
ON table_name
USING GIN(column_name gin_trgm_ops);
```
The key here is the `GIN(column_name gin_trgm_ops)` part. This creates a [GIN
index][gin-index] with the operator class set to `gin_trgm_ops`. These indexes
_can_ be used by `ILIKE` / `LIKE` and can lead to greatly improved performance.
One downside of these indexes is that they can easily get quite large (depending
on the amount of data indexed).
To keep naming of these indexes consistent please use the following naming
pattern:
index_TABLE_on_COLUMN_trigram
For example, a GIN/trigram index for `issues.title` would be called
`index_issues_on_title_trigram`.
Due to these indexes taking quite some time to be built they should be built
concurrently. This can be done by using `CREATE INDEX CONCURRENTLY` instead of
just `CREATE INDEX`. Concurrent indexes can _not_ be created inside a
transaction. Transactions for migrations can be disabled using the following
pattern:
```ruby
class MigrationName < ActiveRecord::Migration
disable_ddl_transaction!
end
```
For example:
```ruby
class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));'
execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));'
end
def down
return unless Gitlab::Database.postgresql?
remove_index :users, :index_on_users_lower_username
remove_index :users, :index_on_users_lower_email
end
end
```
## Plucking IDs
This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a
set of values into memory only to use them as an argument for another query. For
example, this will make the database **very** sad:
```ruby
projects = Project.all.pluck(:id)
MergeRequest.where(source_project_id: projects)
```
Instead you can just use sub-queries which perform far better:
```ruby
MergeRequest.where(source_project_id: Project.all.select(:id))
```
The _only_ time you should use `pluck` is when you actually need to operate on
the values in Ruby itself (e.g. write them to a file). In almost all other cases
you should ask yourself "Can I not just use a sub-query?".
## Use UNIONs
UNIONs aren't very commonly used in most Rails applications but they're very
powerful and useful. In most applications queries tend to use a lot of JOINs to
get related data or data based on certain criteria, but JOIN performance can
quickly deteriorate as the data involved grows.
For example, if you want to get a list of projects where the name contains a
value _or_ the name of the namespace contains a value most people would write
the following query:
```sql
SELECT *
FROM projects
JOIN namespaces ON namespaces.id = projects.namespace_id
WHERE projects.name ILIKE '%gitlab%'
OR namespaces.name ILIKE '%gitlab%';
```
Using a large database this query can easily take around 800 milliseconds to
run. Using a UNION we'd write the following instead:
```sql
SELECT projects.*
FROM projects
WHERE projects.name ILIKE '%gitlab%'
UNION
SELECT projects.*
FROM projects
JOIN namespaces ON namespaces.id = projects.namespace_id
WHERE namespaces.name ILIKE '%gitlab%';
```
This query in turn only takes around 15 milliseconds to complete while returning
the exact same records.
This doesn't mean you should start using UNIONs everywhere, but it's something
to keep in mind when using lots of JOINs in a query and filtering out records
based on the joined data.
GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a UNION
of multiple `ActiveRecord::Relation` objects. You can use this class as
follows:
```ruby
union = Gitlab::SQL::Union.new([projects, more_projects, ...])
Project.from("(#{union.to_sql}) projects")
```
## Ordering by Creation Date
When ordering records based on the time they were created you can simply order
by the `id` column instead of ordering by `created_at`. Because IDs are always
unique and incremented in the order that rows are created this will produce the
exact same results. This also means there's no need to add an index on
`created_at` to ensure consistent performance as `id` is already indexed by
default.
[gin-index]: http://www.postgresql.org/docs/current/static/gin.html
......@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Please explore [web hooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
......
......@@ -467,12 +467,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj
### Initial Login
Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in:
Visit YOUR_SERVER in your web browser for your first GitLab login.
root
5iveL!fe
If you didn't [provide a root password during setup](#initialize-database-and-activate-advanced-features),
you'll be redirected to a password reset screen to provide the password for the
initial administrator account. Enter your desired password and you'll be
redirected back to the login screen.
**Important Note:** On login you'll be prompted to change the password.
The default account's username is **root**. Provide the password you created
earlier and login. After login you can change the username if you wish.
**Enjoy!**
......
......@@ -39,3 +39,34 @@ please see the [project_services directory][projects-code].
[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
[Project Service]: ../project_services/project_services.md
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
## SSL certificate errors
When trying to integrate GitLab with services that are using self-signed certificates,
it is very likely that SSL certificate errors will occur on different parts of the
application, most likely Sidekiq. There are 2 approaches you can take to solve this:
1. Add the root certificate to the trusted chain of the OS.
1. If using Omnibus, you can add the certificate to GitLab's trusted certificates.
**OS main trusted chain**
This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html)
has all the information you need to add a certificate to the main trusted chain.
This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu)
at SuperUser also has relevant information.
**Omnibus Trusted Chain**
It is enough to concatenate the certificate to the main trusted certificate:
```bash
cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
```
After that restart GitLab with:
```bash
sudo gitlab-ctl restart
```
......@@ -204,3 +204,25 @@ When setting `method: ssl`, the underlying authentication method used by
`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
the LDAP server before any LDAP-protocol data is exchanged but no validation of
the LDAP server's SSL certificate is performed.
## Troubleshooting
### Invalid credentials when logging in
Make sure the user you are binding with has enough permissions to read the user's
tree and traverse it.
Also make sure that the `user_filter` is not blocking otherwise valid users.
To make sure that the LDAP settings are correct and GitLab can see your users,
execute the following command:
```bash
# For Omnibus installations
sudo gitlab-rake gitlab:ldap:check
# For installations from source
sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
```
......@@ -133,12 +133,16 @@ will be returned to GitLab and will be signed in.
## Troubleshooting
### 500 error after login
If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page,
this likely indicates that GitLab could not get the email address for the SAML user.
Make sure the IdP provides a claim containing the user's email address, using claim name
`email` or `mail`.
### Redirect back to login screen with no evident error
If after signing in into your SAML server you are redirected back to the sign in page and
no error is displayed, check your `production.log` file. It will most likely contain the
message `Can't verify CSRF token authenticity`. This means that there is an error during
......@@ -147,4 +151,36 @@ the SAML request, but this error never reaches GitLab due to the CSRF check.
To bypass this you can add `skip_before_action :verify_authenticity_token` to the
`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab,
where it can then be seen in the usual logs, or as a flash message in the login
screen.
\ No newline at end of file
screen.
### Invalid audience
This error means that the IdP doesn't recognize GitLab as a valid sender and
receiver of SAML requests. Make sure to add the GitLab callback URL to the approved
audiences of the IdP server.
### Missing claims
The IdP server needs to pass certain information in order for GitLab to either
create an account, or match the login information to an existing account. `email`
is the minimum amount of information that needs to be passed. If the IdP server
is not providing this information, all SAML requests will fail.
Make sure this information is provided.
### Key validation error, Digest mismatch or Fingerprint mismatch
These errors all come from a similar place, the SAML certificate. SAML requests
need to be validated using a fingerprint, a certificate or a validator.
For this you need take the following into account:
- If no certificate is provided in the settings, a fingerprint or fingerprint
validator needs to be provided and the response from the server must contain
a certificate (`<ds:KeyInfo><ds:X509Data><ds:X509Certificate>`)
- If a certificate is provided in the settings, it is no longer necessary for
the request to contain one. In this case the fingerprint or fingerprint
validators are optional
Make sure that one of the above described scenarios is valid, or the requests will
fail with one of the mentioned errors.
\ No newline at end of file
......@@ -2,19 +2,11 @@
## On Slack
To enable Slack integration you must create an Incoming WebHooks integration on Slack;
To enable Slack integration you must create an Incoming WebHooks integration on Slack:
1. [Sign in to Slack](https://slack.com/signin)
1. Select **Apps & Custom Integrations** from the dropdown next to your team name.
1. Click the **Configure** link (right-upper corner).
1. Select the **Custom integrations** tab.
1. Click the **Incoming WebHooks** row.
1. Click the **Add configuration** button.
1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
1. Choose the channel name you want to send notifications to.
......
......@@ -207,6 +207,7 @@ GFM also recognizes certain cross-project references:
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
| `namespace/project~"Some label"` | issues with given label |
## Task Lists
......
......@@ -219,3 +219,16 @@ You can see from the above image that there are four references to GitLab:
[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website"
[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service"
[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post"
## Troubleshooting
### GitLab is unable to comment on a ticket
Make sure that the user you set up for GitLab to communicate with JIRA has the
correct access permission to post comments on a ticket and to also transition the
ticket, if you'd like GitLab to also take care of closing them.
### GitLab is unable to close a ticket
Make sure the the `Transition ID` you set within the JIRA settings matches the
one your project needs to close a ticket.
......@@ -64,6 +64,9 @@ sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
......
......@@ -62,7 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment
# MySQL
sudo -u git -H bundle install --without development test postgres --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
......
......@@ -4,7 +4,7 @@
Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome.
*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.*
*Make sure you view this [upgrade guide from the 'master' branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md) for the most up to date instructions.*
GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version.
......
......@@ -582,7 +582,6 @@ X-Gitlab-Event: Note Hook
"created_at": "2015-04-09 02:40:38 UTC",
"updated_at": "2015-04-09 02:40:38 UTC",
"file_name": "test.rb",
"expires_at": null,
"type": "ProjectSnippet",
"visibility_level": 0
}
......
......@@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with
## Configuration
Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on.
Git LFS objects can be large in size. By default, they are stored on the server
GitLab is installed on.
There are two configuration options to help GitLab server administrators:
......@@ -37,5 +38,8 @@ In `config/gitlab.yml`:
## Known limitations
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
is not supported
* Currently, removing LFS objects from GitLab Git LFS storage is not supported
* LFS authentications via SSH is not supported for the time being
* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
# Git LFS
Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git.
The general recommendation is to not have Git repositories larger than 1GB to preserve performance.
Managing large files such as audio, video and graphics files has always been one
of the shortcomings of Git. The general recommendation is to not have Git repositories
larger than 1GB to preserve performance.
GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain
environments it is not always convenient to use different commands to differentiate between the large files and regular ones.
GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
(EE only), however in certain environments it is not always convenient to use
different commands to differentiate between the large files and regular ones.
Git LFS makes this simpler for the end user by removing the requirement to learn new commands.
Git LFS makes this simpler for the end user by removing the requirement to
learn new commands.
## How it works
Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests.
Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file.
Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
to authorize client requests. Once the request is authorized, Git LFS client receives
instructions from where to fetch or where to push the large file.
## GitLab server configuration
......@@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do
## Known limitations
* Git LFS v1 original API is not supported since it was deprecated early in LFS development
* Git LFS v1 original API is not supported since it was deprecated early in LFS
development
* When SSH is set as a remote, Git LFS objects still go through HTTPS
* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting)
* Any Git LFS request will ask for HTTPS credentials to be provided so good Git
credentials store is recommended
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
to add the URL to Git config manually (see #troubleshooting)
## Using Git LFS
Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS:
For example, if you want to upload a very large file and check it into your Git repository:
Lets take a look at the workflow when you need to check large files into your Git
repository with Git LFS. For example, if you want to upload a very large file and
check it into your Git repository:
```bash
git clone git@gitlab.example.com:group/project.git
......@@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project
git lfs track "*.iso" # select the file extensions that you want to treat as large files
```
Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension:
Once a certain file extension is marked for tracking as a LFS object you can use
Git as usual without having to redo the command to track a file with the same extension:
```bash
cp ~/tmp/debian.iso ./ # copy a large file into the current directory
......@@ -49,13 +58,17 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication.
Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP
authentication.
```bash
git clone git@gitlab.example.com:group/project.git
```
If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`:
If you already cloned the repository and you want to get the latest LFS object
that are on the remote repository, eg. from branch `master`:
```bash
git lfs fetch master
......@@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project.
* Project is not allowed to access the LFS object
LFS object you are trying to push to the project or fetch from the project is not available to the project anymore.
Probably the object was removed from the server.
LFS object you are trying to push to the project or fetch from the project is not
available to the project anymore. Probably the object was removed from the server.
* Local git repository is using deprecated LFS API
......@@ -89,16 +102,26 @@ git lfs logs last
If the status `error 501` is shown, it is because:
* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support.
* Git LFS support is not enabled on the GitLab server. Check with your GitLab
administrator why Git LFS is not enabled on the server. See
[LFS administration documentation](lfs_administration.md) for instructions
on how to enable LFS support.
* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported.
* Git LFS client version is not supported by GitLab server. Check your Git LFS
version with `git lfs version`. Check the Git config of the project for traces
of deprecated API with `git lfs -l`. If `batch = false` is set in the config,
remove the line and try to update your Git LFS client. Only version 1.0.1 and
newer are supported.
### getsockopt: connection refused
If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP.
If you push a LFS object to a project and you receive an error similar to:
`Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab
instance is being served on HTTP.
This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config.
This behaviour is caused by Git LFS using HTTPS connections by default when a
`lfsurl` is not set in the Git config.
To prevent this from happening, set the lfs url in project Git config:
......@@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o
### Credentials are always required when pushing an object
Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required.
Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
the LFS object on every push for every object, user HTTPS credentials are required.
By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
By default, Git has support for remembering the credentials for each repository
you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
For example, you can tell Git to remember the password for a period of time in which you expect to push the objects:
For example, you can tell Git to remember the password for a period of time in
which you expect to push the objects:
```bash
git config --global credential.helper 'cache --timeout=3600'
```
This will remember the credentials for an hour after which Git operations will require re-authentication.
This will remember the credentials for an hour after which Git operations will
require re-authentication.
If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
If you are using OS X you can use `osxkeychain` to store and encrypt your credentials.
For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
\ No newline at end of file
More details about various methods of storing the user credentials can be found
on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
\ No newline at end of file
......@@ -10,3 +10,8 @@ Feature: Dashboard Archived Projects
Scenario: I should see non-archived projects on dashboard
Then I should see "Shop" project link
And I should not see "Forum" project link
Scenario: I toggle show of archived projects on dashboard
When I click "Show archived projects" link
Then I should see "Shop" project link
And I should see "Forum" project link
......@@ -140,4 +140,4 @@ Feature: Explore Projects
When I visit the explore starred projects
Then I should see project "Community"
And I should see project "Internal"
And I should see project "Archive"
And I should not see project "Archive"
......@@ -28,3 +28,20 @@ Feature: Group Milestones
And I fill milestone name
When I press create mileston button
Then milestone in each project should be created
Scenario: I should see Issues listed with labels
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
Then I should see the "bug" label
And I should see the "feature" label
And I should see the project name in the Issue row
Scenario: I should see the Labels tab
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
And I click on the "Labels" tab
Then I should see the list of labels
......@@ -58,14 +58,6 @@ Feature: Project Issues
Then I should see comment "XML attached"
And I should see an error alert section within the comment form
@javascript
Scenario: Visiting Issues after leaving a comment
Given I visit issue page "Release 0.4"
And I leave a comment like "XML attached"
And I visit project "Shop" issues page
And I sort the list by "Last updated"
Then I should see "Release 0.4" at the top
@javascript
Scenario: Visiting Issues after being sorted the list
Given I visit project "Shop" issues page
......
......@@ -26,6 +26,16 @@ Feature: Project Merge Requests
When I visit project "Shop" merge requests page
Then I should see "other_branch" branch
Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
Given project "Shop" have "Bug NS-07" open merge request with rebased branch
When I visit merge request page "Bug NS-07"
Then I should not see the diverged commits count
Scenario: I should see the numbers of diverged commits if the branch diverged from the target
Given project "Shop" have "Bug NS-08" open merge request with diverged branch
When I visit merge request page "Bug NS-08"
Then I should see the diverged commits count
Scenario: I should see rejected merge requests
Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests
......@@ -76,15 +86,6 @@ Feature: Project Merge Requests
And I leave a comment like "XML attached"
Then I should see comment "XML attached"
@javascript
Scenario: Visiting Merge Requests after leaving a comment
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-04"
And I leave a comment like "XML attached"
And I visit project "Shop" merge requests page
And I sort the list by "Last updated"
Then I should see "Bug NS-04" at the top
@javascript
Scenario: Visiting Merge Requests after being sorted the list
Given I visit project "Shop" merge requests page
......@@ -118,16 +119,6 @@ Feature: Project Merge Requests
And I sort the list by "Least popular"
Then The list should be sorted by "Least popular"
@javascript
Scenario: Visiting Merge Requests after commenting on diffs
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on diff
And I visit project "Shop" merge requests page
And I sort the list by "Last updated"
Then I should see "Bug NS-05" at the top
@javascript
Scenario: I comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
......
......@@ -19,4 +19,8 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps
step 'I should see "Forum" project link' do
expect(page).to have_link "Forum"
end
step 'I click "Show archived projects" link' do
click_link "Show archived projects"
end
end
......@@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
step 'I click on one group milestone' do
milestones = Milestone.where(title: 'GL-113')
@global_milestone = GlobalMilestone.new('GL-113', milestones)
click_link 'GL-113'
end
......@@ -33,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
step 'I should see group milestone with all issues and MRs assigned to that milestone' do
expect(page).to have_content('Milestone GL-113')
expect(page).to have_content('Progress: 0 closed – 3 open')
expect(page).to have_content('3 issues: 3 open and 0 closed')
issue = Milestone.find_by(name: 'GL-113').issues.first
expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
end
......@@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
end
step 'I should see the "bug" label' do
page.within('#tab-issues') do
expect(page).to have_content 'bug'
end
end
step 'I should see the "feature" label' do
page.within('#tab-issues') do
expect(page).to have_content 'bug'
end
end
step 'I should see the project name in the Issue row' do
page.within('#tab-issues') do
@global_milestone.projects.each do |project|
expect(page).to have_content project.name
end
end
end
step 'I click on the "Labels" tab' do
page.within('.nav-links') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
end
step 'I should see the list of labels' do
page.within('#tab-labels') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
end
private
def group_milestone
......@@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
project = create :project, path: path, group: group
milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
create(:label, project: project, title: 'feature')
create :issue,
project: project,
assignee: current_user,
......@@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
due_date: '2114-08-20',
description: 'Lorem Ipsum is simply dummy text'
create :issue,
issue = create :issue,
project: project,
assignee: current_user,
author: current_user,
milestone: milestone
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end
end
end
......@@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
end
step 'I should see "Release 0.4" at the top' do
expect(page.find('ul.content-list.issues-list li.issue:first-child')).to have_content("Release 0.4")
end
def filter_issue(text)
fill_in 'issue_search', with: text
end
......
......@@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I should see 3 issues' do
expect(page).to have_selector('#tab-issues li.issue-row', count: 4)
expect(page).to have_selector('#tab-issues li.issuable-row', count: 4)
end
step 'I click link to remove milestone' do
......
......@@ -60,7 +60,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).not_to have_content "Feature NS-03"
end
step 'I should not see "Bug NS-04" in merge requests' do
expect(page).not_to have_content "Bug NS-04"
end
......@@ -121,6 +120,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first)
end
step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
create(:merge_request, :rebased,
title: "Bug NS-07",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Shop" have "Bug NS-08" open merge request with diverged branch' do
create(:merge_request, :diverged,
title: "Bug NS-08",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Shop" have "Feature NS-03" closed merge request' do
create(:closed_merge_request,
title: "Feature NS-03",
......@@ -490,12 +505,16 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
step 'I should see "Bug NS-05" at the top' do
expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05")
step 'I should see the diverged commits count' do
page.within ".mr-source-target" do
expect(page).to have_content /([0-9]+ commits behind)/
end
end
step 'I should see "Bug NS-04" at the top' do
expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04")
step 'I should not see the diverged commits count' do
page.within ".mr-source-target" do
expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
end
end
def merge_request
......
......@@ -29,7 +29,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
@project = create(:project, :public)
@project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
......
......@@ -36,7 +36,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
@project = create(:project, :public)
@project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
......
......@@ -144,11 +144,4 @@ module SharedNote
expect(page).to have_content("+1 Awesome!")
end
end
step 'I sort the list by "Last updated"' do
find('button.dropdown-toggle.btn').click
page.within('ul.dropdown-menu.dropdown-menu-align-right li') do
click_link "Last updated"
end
end
end
......@@ -388,13 +388,19 @@ module SharedPaths
end
step 'I visit merge request page "Bug NS-04"' do
mr = MergeRequest.find_by(title: "Bug NS-04")
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
visit merge_request_path("Bug NS-04")
end
step 'I visit merge request page "Bug NS-05"' do
mr = MergeRequest.find_by(title: "Bug NS-05")
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
visit merge_request_path("Bug NS-05")
end
step 'I visit merge request page "Bug NS-07"' do
visit merge_request_path("Bug NS-07")
end
step 'I visit merge request page "Bug NS-08"' do
visit merge_request_path("Bug NS-08")
end
step 'I visit merge request page "Bug CO-01"' do
......@@ -503,6 +509,11 @@ module SharedPaths
Project.find_by!(name: 'Shop')
end
def merge_request_path(title)
mr = MergeRequest.find_by(title: title)
namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
end
# ----------------------------------------
# Errors
# ----------------------------------------
......
......@@ -14,6 +14,7 @@ require 'sidekiq/testing/inline'
require_relative 'capybara'
require_relative 'db_cleaner'
require_relative 'rerun'
%w(select2_helper test_env repo_helpers).each do |f|
require Rails.root.join('spec', 'support', f)
......
# The spinach-rerun-reporter doesn't define the on_undefined_step
# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb
module Spinach
class Reporter
class Rerun
def on_undefined_step(step_data, failure, step_definitions = nil)
super step_data, failure, step_definitions
# save feature file and scenario line
@rerun << "#{current_feature.filename}:#{current_scenario.line}"
end
end
end
end
......@@ -23,6 +23,8 @@ module API
end
class UserFull < User
expose :last_sign_in_at
expose :confirmed_at
expose :email
expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
......@@ -141,7 +143,7 @@ module API
class ProjectSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :expires_at, :updated_at, :created_at
expose :updated_at, :created_at
end
class ProjectEntity < Grape::Entity
......
......@@ -94,6 +94,8 @@ module Banzai
object_link_filter(link, object_class.link_reference_pattern, link_text: text)
end
end
doc
end
# Replace references (like `!123` for merge requests) in text with links
......
module Banzai
module Filter
# HTML filter that replaces label references with links.
class LabelReferenceFilter < ReferenceFilter
# Public: Find label references in text
#
# LabelReferenceFilter.references_in(text) do |match, id, name|
# "<a href=...>#{Label.find(id)}</a>"
# end
#
# text - String text to search.
#
# Yields the String match, an optional Integer label ID, and an optional
# String label name.
#
# Returns a String replaced with the return of the block.
def self.references_in(text)
text.gsub(Label.reference_pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name]
end
class LabelReferenceFilter < AbstractReferenceFilter
def self.object_class
Label
end
def self.referenced_by(node)
{ label: LazyReference.new(Label, node.attr("data-label")) }
def find_object(project, id)
project.labels.find(id)
end
def call
replace_text_nodes_matching(Label.reference_pattern) do |content|
label_link_filter(content)
end
replace_link_nodes_with_href(Label.reference_pattern) do |link, text|
label_link_filter(link, link_text: text)
def self.references_in(text, pattern = Label.reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~
end
end
# Replace label references in text with links to the label specified.
#
# text - String text to replace references in.
#
# Returns a String with label references replaced with links. All links
# have `gfm` and `gfm-label` class names attached for styling.
def label_link_filter(text, link_text: nil)
project = context[:project]
self.class.references_in(text) do |match, id, name|
params = label_params(id, name)
if label = project.labels.find_by(params)
url = url_for_label(project, label)
klass = reference_class(:label)
data = data_attribute(
original: link_text || match,
project: project.id,
label: label.id
)
def references_in(text, pattern = Label.reference_pattern)
text.gsub(pattern) do |match|
project = project_from_ref($~[:project])
params = label_params($~[:label_id].to_i, $~[:label_name])
label = project.labels.find_by(params)
text = link_text || render_colored_label(label)
%(<a href="#{url}" #{data}
class="#{klass}">#{escape_once(text)}</a>)
if label
yield match, label.id, $~[:project], $~
else
match
end
end
end
def url_for_label(project, label)
def url_for_object(label, project)
h = Gitlab::Application.routes.url_helpers
h.namespace_project_issues_url( project.namespace, project, label_name: label.name,
only_path: context[:only_path])
h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
only_path: context[:only_path])
end
def render_colored_label(label)
LabelsHelper.render_colored_label(label)
def object_link_text(object, matches)
if context[:project] == object.project
LabelsHelper.render_colored_label(object)
else
LabelsHelper.render_colored_cross_project_label(object)
end
end
# Parameters to pass to `Label.find_by` based on the given arguments
......
......@@ -76,7 +76,7 @@ module Gitlab
project.issues.create!(
description: body,
title: issue["title"],
state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened',
state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
author_id: gl_user_id(project, reporter)
)
end
......
# This task will generate a standard and Retina sprite of all of the current
# Gemojione Emojis, with the accompanying SCSS map.
#
# It will not appear in `rake -T` output, and the dependent gems are not
# included in the Gemfile by default, because this task will only be needed
# occasionally, such as when new Emojis are added to Gemojione.
begin
require 'sprite_factory'
require 'rmagick'
rescue LoadError
# noop
end
namespace :gemojione do
task sprite: :environment do
check_requirements!
SIZE = 20
RETINA = SIZE * 2
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), SIZE)
end
end
style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
SpriteFactory.run!(File.join(tmpdir, 'images'), {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
style: :scss,
nocomments: true,
pngcrush: true,
layout: :packed
})
# SpriteFactory's SCSS is a bit too verbose for our purposes here, so
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
f.puts
f.puts <<-CSS.strip_heredoc
.emoji-icon {
background-image: image-url('emoji.png');
background-repeat: no-repeat;
height: #{SIZE}px;
width: #{SIZE}px;
@media only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (min--moz-device-pixel-ratio: 2),
only screen and (-o-min-device-pixel-ratio: 2/1),
only screen and (min-device-pixel-ratio: 2),
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
background-image: image-url('emoji@2x.png');
background-size: 840px 820px;
}
}
CSS
end
end
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), RETINA)
end
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.run!(File.join(tmpdir, 'images'), {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
pngcrush: true,
layout: :packed
})
end
end
def check_requirements!
return if defined?(SpriteFactory) && defined?(Magick)
puts <<-MSG.strip_heredoc
This task is disabled by default and should only be run when the Gemojione
gem is updated with new Emojis.
To enable this task, *temporarily* add the following lines to Gemfile and
re-bundle:
gem 'sprite-factory'
gem 'rmagick'
MSG
exit 1
end
def resize!(image_path, size)
# Resize the image in-place, save it, and free the object
image = Magick::Image.read(image_path).first
image.resize!(size, size)
image.write(image_path) { self.quality = 100 }
image.destroy!
end
end
......@@ -4,53 +4,59 @@ namespace :spinach do
namespace :project do
desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features"
task :half do
cmds = [
%W(rake gitlab:setup),
%W(spinach --tags @project_commits,@project_issues,@project_merge_requests),
]
run_commands(cmds)
run_spinach_tests('@project_commits,@project_issues,@project_merge_requests')
end
desc "GitLab | Spinach | Run remaining project spinach features"
task :rest do
cmds = [
%W(rake gitlab:setup),
%W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests),
]
run_commands(cmds)
run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests')
end
end
desc "GitLab | Spinach | Run project spinach features"
task :project do
cmds = [
%W(rake gitlab:setup),
%W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets),
]
run_commands(cmds)
run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets')
end
desc "GitLab | Spinach | Run other spinach features"
task :other do
cmds = [
%W(rake gitlab:setup),
%W(spinach --tags @admin,@dashboard,@profile,@public,@snippets),
]
run_commands(cmds)
run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets')
end
desc "GitLab | Spinach | Run other spinach features"
task :builds do
run_spinach_tests('@builds')
end
end
desc "GitLab | Run spinach"
task :spinach do
cmds = [
%W(rake gitlab:setup),
%W(spinach),
]
run_commands(cmds)
run_spinach_tests(nil)
end
def run_command(cmd)
system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
end
def run_commands(cmds)
cmds.each do |cmd|
system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
def run_spinach_command(args)
run_command(%w(spinach -r rerun) + args)
end
def run_spinach_tests(tags)
#run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!')
success = run_spinach_command(%W(--tags #{tags}))
3.times do |_|
break if success
break unless File.exists?('tmp/spinach-rerun.txt')
tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
puts ''
puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red
puts ''
sleep(3)
success = run_spinach_command(tests)
end
raise("spinach tests for #{tags} failed!") unless success
end
#!/bin/bash
retry() {
for i in $(seq 1 3); do
if eval "$@"; then
return 0
fi
sleep 3s
echo "Retrying..."
done
return 1
}
if [ -f /.dockerinit ]; then
mkdir -p vendor
if [ ! -e vendor/phantomjs_1.9.8-0jessie_amd64.deb ]; then
# Install phantomjs package
pushd vendor
if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then
wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
mv phantomjs_1.9.8-0jessie_amd64.deb vendor/
fi
dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb
dpkg -i phantomjs_1.9.8-0jessie_amd64.deb
popd
apt-get update -qq
apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip
# Try to install packages
retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml
......@@ -20,7 +34,7 @@ if [ -f /.dockerinit ]; then
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
export FLAGS=(--path vendor)
export FLAGS=(--path vendor --retry 3)
else
export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
cp config/database.yml.mysql config/database.yml
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, visibility_level: Project::PUBLIC) }
let(:project) { create(:project, :public) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) }
......
FactoryGirl.define do
factory :email do
user
email do
FFaker::Internet.email('alias')
end
factory :another_email do
email do
FFaker::Internet.email('another.alias')
end
end
email { FFaker::Internet.email('alias') }
end
end
......@@ -69,6 +69,16 @@ FactoryGirl.define do
target_branch "master"
end
trait :rebased do
source_branch "markdown"
target_branch "improve/awesome"
end
trait :diverged do
source_branch "feature"
target_branch "master"
end
trait :merge_when_build_succeeds do
merge_when_build_succeeds true
merge_user author
......
FactoryGirl.define do
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
trait :public do
visibility_level PersonalSnippet::PUBLIC
end
trait :internal do
visibility_level PersonalSnippet::INTERNAL
end
trait :private do
visibility_level PersonalSnippet::PRIVATE
end
end
end
......@@ -2,6 +2,26 @@ FactoryGirl.define do
factory :project_member do
user
project
access_level { ProjectMember::MASTER }
master
trait :guest do
access_level ProjectMember::GUEST
end
trait :reporter do
access_level ProjectMember::REPORTER
end
trait :developer do
access_level ProjectMember::DEVELOPER
end
trait :master do
access_level ProjectMember::MASTER
end
trait :owner do
access_level ProjectMember::OWNER
end
end
end
FactoryGirl.define do
factory :project_snippet do
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project
author
title
content
file_name
end
end
FactoryGirl.define do
factory :service do
type ""
title "GitLab CI"
project
end
end
......@@ -12,5 +12,17 @@ FactoryGirl.define do
title
content
file_name
trait :public do
visibility_level Snippet::PUBLIC
end
trait :internal do
visibility_level Snippet::INTERNAL
end
trait :private do
visibility_level Snippet::PRIVATE
end
end
end
......@@ -5,15 +5,14 @@ describe SnippetsFinder do
let(:user1) { create :user }
let(:group) { create :group }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do
before do
@snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE)
@snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL)
@snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC)
@snippet1 = create(:personal_snippet, :private)
@snippet2 = create(:personal_snippet, :internal)
@snippet3 = create(:personal_snippet, :public)
end
it "returns all private and internal snippets" do
......@@ -31,9 +30,9 @@ describe SnippetsFinder do
context ':by_user filter' do
before do
@snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user)
@snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user)
@snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user)
@snippet1 = create(:personal_snippet, :private, author: user)
@snippet2 = create(:personal_snippet, :internal, author: user)
@snippet3 = create(:personal_snippet, :public, author: user)
end
it "returns all public and internal snippets" do
......@@ -75,9 +74,9 @@ describe SnippetsFinder do
context 'by_project filter' do
before do
@snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1)
@snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1)
@snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1)
@snippet1 = create(:project_snippet, :private, project: project1)
@snippet2 = create(:project_snippet, :internal, project: project1)
@snippet3 = create(:project_snippet, :public, project: project1)
end
it "returns public snippets for unauthorized user" do
......@@ -93,7 +92,7 @@ describe SnippetsFinder do
end
it "returns all snippets for project members" do
project1.team << [user, :developer]
project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
......
......@@ -209,7 +209,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by ID: <%= simple_label.to_reference %>
- Label by name: <%= Label.reference_prefix %><%= simple_label.name %>
- Label by name in quotes: <%= label.to_reference(:name) %>
- Label by name in quotes: <%= label.to_reference(format: :name) %>
- Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
- Link to label by reference: [Label](<%= label.to_reference %>)
......
......@@ -58,7 +58,7 @@ describe VisibilityLevelHelper do
describe "skip_level?" do
describe "forks" do
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
let(:project) { create(:project, :internal) }
let(:fork_project) { create(:forked_project_with_submodules) }
before do
......@@ -74,7 +74,7 @@ describe VisibilityLevelHelper do
end
describe "non-forked project" do
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
let(:project) { create(:project, :internal) }
it "skips levels" do
expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
......@@ -84,7 +84,7 @@ describe VisibilityLevelHelper do
end
describe "Snippet" do
let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
let(:snippet) { create(:snippet, :internal) }
it "skips levels" do
expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
......
......@@ -111,7 +111,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) }
let(:reference) { label.to_reference(:name) }
let(:reference) { label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
......@@ -176,4 +176,29 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(result[:references][:label]).to eq [label]
end
end
describe 'cross project label references' do
let(:another_project) { create(:empty_project, :public) }
let(:project_name) { another_project.name_with_namespace }
let(:label) { create(:label, project: another_project, color: '#00ff00') }
let(:reference) { label.to_reference(project) }
let!(:result) { reference_filter("See #{reference}") }
it 'points to referenced project issues page' do
expect(result.css('a').first.attr('href'))
.to eq urls.namespace_project_issues_url(another_project.namespace,
another_project,
label_name: label.name)
end
it 'has valid color' do
expect(result.css('a span').first.attr('style'))
.to match /background-color: #00ff00/
end
it 'contains cross project content' do
expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}"
end
end
end
require 'spec_helper'
describe Gitlab::BitbucketImport::Importer, lib: true do
before do
Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
end
let(:statuses) do
[
"open",
"resolved",
"on hold",
"invalid",
"duplicate",
"wontfix",
"closed" # undocumented status
]
end
let(:sample_issues_statuses) do
issues = []
statuses.map.with_index do |status, index|
issues << {
local_id: index,
status: status,
title: "Issue #{index}",
content: "Some content to issue #{index}"
}
end
issues
end
let(:project_identifier) { 'namespace/repo' }
let(:data) do
{
bb_session: {
bitbucket_access_token: "123456",
bitbucket_access_token_secret: "secret"
}
}
end
let(:project) do
create(
:project,
import_source: project_identifier,
import_data: ProjectImportData.new(data: data)
)
end
let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
let(:issues_statuses_sample_data) do
{
count: sample_issues_statuses.count,
issues: sample_issues_statuses
}
end
context 'issues statuses' do
before do
stub_request(
:get,
"https://bitbucket.org/api/1.0/repositories/#{project_identifier}"
).to_return(status: 200, body: { has_issues: true }.to_json)
stub_request(
:get,
"https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0"
).to_return(status: 200, body: issues_statuses_sample_data.to_json)
sample_issues_statuses.each_with_index do |issue, index|
stub_request(
:get,
"https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments"
).to_return(
status: 200,
body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json
)
end
end
it 'map statuses to open or closed' do
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2)
end
end
end
......@@ -59,18 +59,42 @@ describe Label, models: true do
context 'using id' do
it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}"
expect(label.to_reference(double('project'))).to eq "~#{label.id}"
end
end
context 'using name' do
it 'returns a String reference to the object' do
expect(label.to_reference(:name)).to eq %(~"#{label.name}")
expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
end
it 'uses id when name contains double quote' do
label = create(:label, name: %q{"irony"})
expect(label.to_reference(:name)).to eq "~#{label.id}"
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
context 'using invalid format' do
it 'raises error' do
expect { label.to_reference(format: :invalid) }
.to raise_error StandardError, /Unknown format/
end
end
context 'cross project reference' do
let(:project) { create(:project) }
context 'using name' do
it 'returns cross reference with label name' do
expect(label.to_reference(project, format: :name))
.to eq %Q(#{label.project.to_reference}~"#{label.name}")
end
end
context 'using id' do
it 'returns cross reference with label id' do
expect(label.to_reference(project, format: :id))
.to eq %Q(#{label.project.to_reference}~#{label.id})
end
end
end
end
......
......@@ -274,6 +274,70 @@ describe MergeRequest, models: true do
end
end
describe '#diverged_commits_count' do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
context 'diverged on same repository' do
subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(5)
end
end
context 'diverged on fork' do
subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(5)
end
end
context 'rebased on fork' do
subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) }
it 'counts commits that are on target branch but not on source branch' do
expect(subject.diverged_commits_count).to eq(0)
end
end
describe 'caching' do
before(:example) do
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
end
it 'caches the output' do
expect(subject).to receive(:compute_diverged_commits_count).
once.
and_return(2)
subject.diverged_commits_count
subject.diverged_commits_count
end
it 'invalidates the cache when the source sha changes' do
expect(subject).to receive(:compute_diverged_commits_count).
twice.
and_return(2)
subject.diverged_commits_count
allow(subject).to receive(:source_sha).and_return('123abc')
subject.diverged_commits_count
end
it 'invalidates the cache when the target sha changes' do
expect(subject).to receive(:compute_diverged_commits_count).
twice.
and_return(2)
subject.diverged_commits_count
allow(subject).to receive(:target_sha).and_return('123abc')
subject.diverged_commits_count
end
end
end
it_behaves_like 'an editable mentionable' do
subject { create(:merge_request) }
......
......@@ -60,7 +60,7 @@ describe Milestone, models: true do
end
it "should recover from dividing by zero" do
expect(milestone.issues).to receive(:count).and_return(0)
expect(milestone.issues).to receive(:size).and_return(0)
expect(milestone.percent_complete).to eq(0)
end
end
......@@ -114,7 +114,6 @@ describe Milestone, models: true do
end
it { expect(milestone.closed_items_count).to eq(1) }
it { expect(milestone.open_items_count).to eq(2) }
it { expect(milestone.total_items_count).to eq(3) }
it { expect(milestone.is_empty?).to be_falsey }
end
......
......@@ -24,7 +24,7 @@ require 'spec_helper'
describe Note, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:noteable) }
it { is_expected.to belong_to(:noteable).touch(true) }
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to have_many(:todos).dependent(:destroy) }
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......
......@@ -68,6 +68,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe 'modules' do
......@@ -561,7 +562,7 @@ describe Project, models: true do
end
describe '#visibility_level_allowed?' do
let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
let(:project) { create(:project, :internal) }
context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
......
......@@ -148,6 +148,12 @@ describe Repository, models: true do
expect(branch.name).to eq('new_feature')
end
it 'calls the after_create_branch hook' do
expect(repository).to receive(:after_create_branch)
repository.add_branch(user, 'new_feature', 'master')
end
end
context 'when pre hooks failed' do
......@@ -405,7 +411,7 @@ describe Repository, models: true do
end
end
describe '#expire_branch_ache' do
describe '#expire_branch_cache' do
# This method is private but we need it for testing purposes. Sadly there's
# no other proper way of testing caching operations.
let(:cache) { repository.send(:cache) }
......@@ -556,11 +562,12 @@ describe Repository, models: true do
end
end
describe '#before_create_tag' do
describe '#before_push_tag' do
it 'flushes the cache' do
expect(repository).to receive(:expire_cache)
expect(repository).to receive(:expire_tag_count_cache)
repository.before_create_tag
repository.before_push_tag
end
end
......@@ -595,4 +602,89 @@ describe Repository, models: true do
repository.after_remove_branch
end
end
describe "#main_language" do
it 'shows the main language of the project' do
expect(repository.main_language).to eq("Ruby")
end
it 'returns nil when the repository is empty' do
allow(repository).to receive(:empty?).and_return(true)
expect(repository.main_language).to be_nil
end
end
describe '#before_remove_tag' do
it 'flushes the tag cache' do
expect(repository).to receive(:expire_tag_count_cache)
repository.before_remove_tag
end
end
describe '#branch_count' do
it 'returns the number of branches' do
expect(repository.branch_count).to be_an_instance_of(Fixnum)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
expect(repository.tag_count).to be_an_instance_of(Fixnum)
end
end
describe '#expire_branch_count_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do
expect(cache).to receive(:expire).with(:branch_count)
repository.expire_branch_count_cache
end
end
describe '#expire_tag_count_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do
expect(cache).to receive(:expire).with(:tag_count)
repository.expire_tag_count_cache
end
end
describe '#add_tag' do
it 'adds a tag' do
expect(repository).to receive(:before_push_tag)
expect_any_instance_of(Gitlab::Shell).to receive(:add_tag).
with(repository.path_with_namespace, '8.5', 'master', 'foo')
repository.add_tag('8.5', 'master', 'foo')
end
end
describe '#rm_branch' do
let(:user) { create(:user) }
it 'removes a branch' do
expect(repository).to receive(:before_remove_branch)
expect(repository).to receive(:after_remove_branch)
repository.rm_branch(user, 'feature')
end
end
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
with(repository.path_with_namespace, '8.5')
repository.rm_tag('8.5')
end
end
end
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......
......@@ -7,8 +7,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
......
......@@ -7,8 +7,8 @@ describe API::API, api: true do
let(:api_user) { user }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) }
let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
let(:commit) { create(:ci_commit, project: project)}
let(:build) { create(:ci_build, commit: commit) }
......
......@@ -6,9 +6,9 @@ describe API::CommitStatus, api: true do
let!(:project) { create(:project) }
let(:commit) { project.repository.commit }
let(:commit_status) { create(:commit_status, commit: ci_commit) }
let(:guest) { create_user(ProjectMember::GUEST) }
let(:reporter) { create_user(ProjectMember::REPORTER) }
let(:developer) { create_user(ProjectMember::DEVELOPER) }
let(:guest) { create_user(:guest) }
let(:reporter) { create_user(:reporter) }
let(:developer) { create_user(:developer) }
let(:sha) { commit.id }
......@@ -201,9 +201,9 @@ describe API::CommitStatus, api: true do
end
end
def create_user(access_level)
def create_user(access_level_trait)
user = create(:user)
create(:project_member, user: user, project: project, access_level: access_level)
create(:project_member, access_level_trait, user: user, project: project)
user
end
end
......@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
......
......@@ -12,7 +12,7 @@ describe API::API, api: true do
end
let(:project_user2) do
create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST)
create(:project_member, :guest, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do
......
......@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) }
let(:project_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
describe "GET /projects/:id/members" do
before { project_member }
......
......@@ -12,8 +12,8 @@ describe API::API, api: true do
let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) }
let(:project_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
create(:project,
......
......@@ -9,8 +9,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tree" do
context "authorized user" do
......
......@@ -28,9 +28,9 @@ describe API::Runners, api: true do
before do
# Set project access for users
create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER)
create(:project_member, user: user, project: project2, access_level: ProjectMember::MASTER)
create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER)
create(:project_member, :master, user: user, project: project)
create(:project_member, :master, user: user, project: project2)
create(:project_member, :reporter, user: user2, project: project)
end
describe 'GET /runners' do
......
......@@ -8,8 +8,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tags" do
let(:tag_name) { project.repository.tag_names.sort.reverse.first }
......
......@@ -8,8 +8,8 @@ describe API::API do
let!(:trigger_token) { 'secure_token' }
let!(:trigger_token_2) { 'secure_token_2' }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
......
......@@ -47,6 +47,8 @@ describe API::API, api: true do
expect(json_response.first.keys).to include 'identities'
expect(json_response.first.keys).to include 'can_create_project'
expect(json_response.first.keys).to include 'two_factor_enabled'
expect(json_response.first.keys).to include 'last_sign_in_at'
expect(json_response.first.keys).to include 'confirmed_at'
end
end
end
......
......@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:variable) { create(:ci_variable, project: project) }
describe 'GET /projects/:id/variables' do
......
require 'spec_helper'
describe DeleteTagService, services: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
let(:tag) { double(:tag, name: '8.5', target: 'abc123') }
describe '#execute' do
before do
allow(repository).to receive(:find_tag).and_return(tag)
end
it 'removes the tag' do
expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
and_return(true)
expect(repository).to receive(:before_remove_tag)
expect(service).to receive(:success)
service.execute('8.5')
end
end
end
......@@ -155,6 +155,23 @@ describe GitPushService, services: true do
end
end
describe "Updates main language" do
context "before push" do
it { expect(project.main_language).to eq(nil) }
end
context "after push" do
before do
@service = execute_service(project, user, @oldrev, @newrev, @ref)
end
it { expect(@service.update_main_language).to eq(true) }
it { expect(project.main_language).to eq("Ruby") }
end
end
describe "Web Hooks" do
context "execute web hooks" do
it "when pushing a branch for the first time" do
......@@ -254,22 +271,24 @@ describe GitPushService, services: true do
allow(project.repository).to receive(:commits_between).
and_return([closing_commit])
project.team << [commit_author, :master]
end
context "to default branches" do
it "closes issues" do
execute_service(project, user, @oldrev, @newrev, @ref )
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(Issue.find(issue.id)).to be_closed
end
it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
execute_service(project, user, @oldrev, @newrev, @ref )
execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
it "doesn't create additional cross-reference notes" do
expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, user, @oldrev, @newrev, @ref )
execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
it "doesn't close issues when external issue tracker is in use" do
......@@ -277,7 +296,7 @@ describe GitPushService, services: true do
# The push still shouldn't create cross-reference notes.
expect do
execute_service(project, user, @oldrev, @newrev, 'refs/heads/hurf' )
execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' )
end.not_to change { Note.where(project_id: project.id, system: true).count }
end
end
......@@ -299,7 +318,6 @@ describe GitPushService, services: true do
end
end
# EE-only tests
context "for jira issue tracker" do
include JiraServiceHelper
......@@ -349,7 +367,7 @@ describe GitPushService, services: true do
}
}.to_json
execute_service(project, user, @oldrev, @newrev, @ref )
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
body: transition_body
).once
......@@ -360,7 +378,7 @@ describe GitPushService, services: true do
body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json
execute_service(project, user, @oldrev, @newrev, @ref )
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: comment_body
).once
......
......@@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do
describe :visibility_level do
let(:user) { create :user, admin: true }
let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
let(:project) { create(:project, :internal) }
let(:forked_project) { create(:forked_project_with_submodules, :internal) }
let(:opts) { {} }
before do
......
......@@ -15,6 +15,7 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'sidekiq/testing/inline'
require 'benchmark/ips'
require 'rspec/retry'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
......@@ -25,6 +26,9 @@ RSpec.configure do |config|
config.use_instantiated_fixtures = false
config.mock_with :rspec
config.verbose_retry = true
config.display_try_failure_messages = true
config.include Devise::TestHelpers, type: :controller
config.include LoginHelpers, type: :feature
config.include LoginHelpers, type: :request
......
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