Commit a5079d21 authored by Baldinof's avatar Baldinof

Merge branch 'master' into markdown_preview_shortcut

parents a603422e d9042e8b
...@@ -12,16 +12,18 @@ cache: ...@@ -12,16 +12,18 @@ cache:
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
- ruby -v - ruby -v
- which ruby - 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 - cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log - touch log/application.log
- touch log/test.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 - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
stages: stages:
......
...@@ -3,16 +3,31 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -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) v 8.6.0 (unreleased)
- Contributions to forked projects are included in calendar - Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea) - 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 issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room - 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) - 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 - 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 - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - 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 - Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip) - Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino) - 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 v 8.5.4
- Do not cache requests for badges (including builds badge) - Do not cache requests for badges (including builds badge)
......
...@@ -263,7 +263,9 @@ group :development, :test do ...@@ -263,7 +263,9 @@ group :development, :test do
gem 'database_cleaner', '~> 1.4.0' gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails', '~> 4.6.0' gem 'factory_girl_rails', '~> 4.6.0'
gem 'rspec-rails', '~> 3.3.0' gem 'rspec-rails', '~> 3.3.0'
gem 'rspec-retry'
gem 'spinach-rails', '~> 0.2.1' 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) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
...@@ -273,7 +275,7 @@ group :development, :test do ...@@ -273,7 +275,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0' gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.8.1' gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0' gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0' gem 'teaspoon-jasmine', '~> 2.2.0'
......
...@@ -552,7 +552,7 @@ GEM ...@@ -552,7 +552,7 @@ GEM
parser (2.2.3.0) parser (2.2.3.0)
ast (>= 1.1, < 3.0) ast (>= 1.1, < 3.0)
pg (0.18.4) pg (0.18.4)
poltergeist (1.8.1) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
cliver (~> 0.3.1) cliver (~> 0.3.1)
multi_json (~> 1.0) multi_json (~> 1.0)
...@@ -679,6 +679,8 @@ GEM ...@@ -679,6 +679,8 @@ GEM
rspec-expectations (~> 3.3.0) rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0) rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0) rspec-support (~> 3.3.0)
rspec-retry (0.4.5)
rspec-core
rspec-support (3.3.0) rspec-support (3.3.0)
rubocop (0.35.1) rubocop (0.35.1)
astrolabe (~> 1.3) astrolabe (~> 1.3)
...@@ -764,6 +766,8 @@ GEM ...@@ -764,6 +766,8 @@ GEM
capybara (>= 2.0.0) capybara (>= 2.0.0)
railties (>= 3) railties (>= 3)
spinach (>= 0.4) spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
spring (1.6.4) spring (1.6.4)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
...@@ -978,7 +982,7 @@ DEPENDENCIES ...@@ -978,7 +982,7 @@ DEPENDENCIES
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
paranoia (~> 2.0) paranoia (~> 2.0)
pg (~> 0.18.2) pg (~> 0.18.2)
poltergeist (~> 1.8.1) poltergeist (~> 1.9.0)
pry-rails pry-rails
quiet_assets (~> 1.0.2) quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1) rack-attack (~> 4.3.1)
...@@ -999,6 +1003,7 @@ DEPENDENCIES ...@@ -999,6 +1003,7 @@ DEPENDENCIES
rouge (~> 1.10.1) rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0) rspec-rails (~> 3.3.0)
rspec-retry
rubocop (~> 0.35.0) rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0) sanitize (~> 2.0)
...@@ -1017,6 +1022,7 @@ DEPENDENCIES ...@@ -1017,6 +1022,7 @@ DEPENDENCIES
six (~> 0.2.0) six (~> 0.2.0)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.6.4) spring (~> 1.6.4)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0) spring-commands-spinach (~> 1.0.0)
......
...@@ -23,7 +23,7 @@ class Dispatcher ...@@ -23,7 +23,7 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
when 'projects:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
......
...@@ -69,7 +69,7 @@ class @Milestone ...@@ -69,7 +69,7 @@ class @Milestone
@bindIssuesSorting() @bindIssuesSorting()
@bindMergeRequestSorting() @bindMergeRequestSorting()
@bindTabsSwitching @bindTabsSwitching()
bindIssuesSorting: -> bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
...@@ -104,7 +104,7 @@ class @Milestone ...@@ -104,7 +104,7 @@ class @Milestone
).disableSelection() ).disableSelection()
bindMergeRequestSorting: -> bindTabsSwitching: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show') currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show') previousTabClass = $(e.relatedTarget).data('show')
...@@ -113,6 +113,7 @@ class @Milestone ...@@ -113,6 +113,7 @@ class @Milestone
$(currentTabClass).removeClass('hidden') $(currentTabClass).removeClass('hidden')
$(currentTabClass).show() $(currentTabClass).show()
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list", connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true, dropOnEmpty: true,
......
...@@ -81,8 +81,9 @@ ...@@ -81,8 +81,9 @@
&::before { &::before {
content: "\f00c"; content: "\f00c";
position: absolute; position: absolute;
left: 4px; left: 5px;
top: 8px; top: 50%;
margin-top: -7px;
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
text-rendering: auto; text-rendering: auto;
...@@ -94,8 +95,8 @@ ...@@ -94,8 +95,8 @@
} }
.dropdown-header { .dropdown-header {
padding-left: 10px; padding-left: 5px;
padding-right: 10px; padding-right: 5px;
color: $dropdown-header-color; color: $dropdown-header-color;
font-size: 13px; font-size: 13px;
line-height: 22px; line-height: 22px;
......
...@@ -19,10 +19,11 @@ li.milestone { ...@@ -19,10 +19,11 @@ li.milestone {
width: 105px; width: 105px;
} }
.issue-row { .issuable-row {
.color-label { .color-label {
border-radius: 2px; border-radius: 2px;
padding: 3px !important; padding: 3px !important;
margin-right: 7px;
} }
// Issue title // Issue title
...@@ -44,20 +45,15 @@ li.milestone { ...@@ -44,20 +45,15 @@ li.milestone {
} }
} }
.issues-sortable-list { .issues-sortable-list, .merge_requests-sortable-list {
.issue-detail { .issuable-detail {
display: block; display: block;
margin-top: 7px;
.issue-number{ .issuable-number {
color: rgba(0,0,0,0.44); color: rgba(0,0,0,0.44);
margin-right: 5px; margin-right: 5px;
} }
.color-label {
padding: 6px 10px;
margin-right: 7px;
margin-top: 10px;
}
.avatar { .avatar {
float: none; float: none;
} }
......
...@@ -26,5 +26,5 @@ ...@@ -26,5 +26,5 @@
margin-right: 10px; margin-right: 10px;
font-size: $gl-font-size; font-size: $gl-font-size;
border: 1px solid; border: 1px solid;
line-height: 40px; line-height: 32px;
} }
...@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController
private private
def group def group
@group = Group.find_by(path: params[:id]) @group ||= Group.find_by(path: params[:id])
end end
def group_params 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 class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
before_action :event_filter before_action :event_filter
def index def index
@projects = current_user.authorized_projects.sorted_by_activity.non_archived @projects = current_user.authorized_projects.sorted_by_activity
@projects = @projects.sort(@sort = params[:sort]) @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace) @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 @last_push = current_user.recent_push
respond_to do |format| respond_to do |format|
...@@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred def starred
@projects = current_user.starred_projects.sorted_by_activity @projects = current_user.starred_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @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 @last_push = current_user.recent_push
@groups = [] @groups = []
......
class Explore::ProjectsController < Explore::ApplicationController class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
def index def index
@projects = ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags) @tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @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.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.non_archived @projects = filter_projects(@projects)
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
...@@ -22,8 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -22,8 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute(current_user)
@projects = @projects.non_archived @projects = filter_projects(@projects)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format| respond_to do |format|
...@@ -38,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -38,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def starred def starred
@projects = ProjectsFinder.new.execute(current_user) @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.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
......
class GroupsController < Groups::ApplicationController class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
...@@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController ...@@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController
def show def show
@last_push = current_user.recent_push if current_user @last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace) @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? @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format| respond_to do |format|
...@@ -98,7 +100,7 @@ class GroupsController < Groups::ApplicationController ...@@ -98,7 +100,7 @@ class GroupsController < Groups::ApplicationController
end end
def load_projects 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 end
# Dont allow unauthorized access to group # Dont allow unauthorized access to group
......
...@@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController ...@@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController
@blob = @repository.blob_at_branch('master', @project.avatar_in_git) @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob if @blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline' headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob) headers['Content-Type'] = safe_content_type(@blob)
......
...@@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def show def show
@issues = @milestone.issues
@users = @milestone.participants.uniq
@merge_requests = @milestone.merge_requests
@labels = @milestone.labels
end end
def create def create
......
...@@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController ...@@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController
if @blob if @blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
if @blob.lfs_pointer? if @blob.lfs_pointer?
send_lfs_object send_lfs_object
else else
......
...@@ -263,11 +263,9 @@ class IssuableFinder ...@@ -263,11 +263,9 @@ class IssuableFinder
def by_label(items) def by_label(items)
if labels? if labels?
if filter_by_no_label? if filter_by_no_label?
items = items. items = items.without_label
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 })
else else
items = items.joins(:labels).where(labels: { title: label_names }) items = items.with_label(label_names)
if projects if projects
items = items.where(labels: { project_id: projects }) items = items.where(labels: { project_id: projects })
......
...@@ -4,7 +4,7 @@ class SnippetsFinder ...@@ -4,7 +4,7 @@ class SnippetsFinder
case filter case filter
when :all then when :all then
snippets(current_user).fresh.non_expired snippets(current_user).fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, params[:user], params[:scope])
when :by_project when :by_project
...@@ -27,7 +27,7 @@ class SnippetsFinder ...@@ -27,7 +27,7 @@ class SnippetsFinder
end end
def by_user(current_user, user, scope) def by_user(current_user, user, scope)
snippets = user.snippets.fresh.non_expired snippets = user.snippets.fresh
return snippets.are_public unless current_user return snippets.are_public unless current_user
...@@ -48,7 +48,7 @@ class SnippetsFinder ...@@ -48,7 +48,7 @@ class SnippetsFinder
end end
def by_project(current_user, project) def by_project(current_user, project)
snippets = project.snippets.fresh.non_expired snippets = project.snippets.fresh
if current_user if current_user
if project.team.member?(current_user.id) if project.team.member?(current_user.id)
......
...@@ -72,7 +72,7 @@ module ApplicationHelper ...@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user = user_or_email user = user_or_email
else else
user = User.find_by(email: user_or_email.downcase) user = User.find_by(email: user_or_email.try(:downcase))
end end
if user if user
......
...@@ -152,4 +152,25 @@ module BlobHelper ...@@ -152,4 +152,25 @@ module BlobHelper
'application/octet-stream' 'application/octet-stream'
end end
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 end
...@@ -211,4 +211,15 @@ module CommitsHelper ...@@ -211,4 +211,15 @@ module CommitsHelper
def clean(string) def clean(string)
Sanitize.clean(string, remove_contents: true) Sanitize.clean(string, remove_contents: true)
end 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 end
module ExploreHelper module ExploreHelper
def explore_projects_filter_path(options={}) def filter_projects_path(options={})
exist_opts = { exist_opts = {
sort: params[:sort], sort: params[:sort],
scope: params[:scope], scope: params[:scope],
...@@ -9,15 +9,7 @@ module ExploreHelper ...@@ -9,15 +9,7 @@ module ExploreHelper
} }
options = exist_opts.merge(options) options = exist_opts.merge(options)
path = request.path
path = if explore_controller?
explore_projects_path
elsif current_action?(:starred)
starred_dashboard_projects_path
else
dashboard_projects_path
end
path << "?#{options.to_param}" path << "?#{options.to_param}"
path path
end end
......
...@@ -50,19 +50,25 @@ module LabelsHelper ...@@ -50,19 +50,25 @@ module LabelsHelper
@project.labels.pluck(:title) @project.labels.pluck(:title)
end end
def render_colored_label(label) def render_colored_label(label, label_suffix = '')
label_color = label.color || Label::DEFAULT_COLOR label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color) text_color = text_color_for_bg(label_color)
# Intentionally not using content_tag here so that this method can be called # Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter # by LabelReferenceFilter
span = %(<span class="label color-label") + span = %(<span class="label color-label") +
%( style="background-color: #{label_color}; color: #{text_color}">) + %(style="background-color: #{label_color}; color: #{text_color}">) +
escape_once(label.name) + '</span>' %(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe span.html_safe
end 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 def suggested_colors
[ [
'#0033CC', '#0033CC',
...@@ -119,5 +125,6 @@ module LabelsHelper ...@@ -119,5 +125,6 @@ module LabelsHelper
end end
# Required for Banzai::Filter::LabelReferenceFilter # 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 end
...@@ -9,6 +9,32 @@ module MilestonesHelper ...@@ -9,6 +9,32 @@ module MilestonesHelper
end end
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) def milestone_progress_bar(milestone)
options = { options = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
......
module SnippetsHelper 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) def reliable_snippet_path(snippet)
if snippet.project_id? if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace, namespace_project_snippet_path(snippet.project.namespace,
......
...@@ -16,6 +16,16 @@ module SortingHelper ...@@ -16,6 +16,16 @@ module SortingHelper
} }
end 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 def sort_title_oldest_updated
'Oldest updated' 'Oldest updated'
end end
......
...@@ -9,6 +9,7 @@ class Ability ...@@ -9,6 +9,7 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject) when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject) when Project then project_abilities(user, subject)
when Issue then issue_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 Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject)
...@@ -424,6 +425,10 @@ class Ability ...@@ -424,6 +425,10 @@ class Ability
end end
end end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private private
def named_abilities(name) def named_abilities(name)
......
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator 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 # Wrap a Gitlab::Git::Blob object, or return nil when given nil
# #
# This method prevents the decorated object from evaluating to "truthy" when # This method prevents the decorated object from evaluating to "truthy" when
......
...@@ -29,12 +29,15 @@ module Issuable ...@@ -29,12 +29,15 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") } scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) } scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) } scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } 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 :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 :join_project, -> { joins(:project) }
scope :references_project, -> { references(: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 ...@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels attr_accessor :title, :labels
alias_attribute :name, :title alias_attribute :name, :title
delegate :color, :description, to: :@first_label
def self.build_collection(labels) def self.build_collection(labels)
labels = labels.group_by(&:title) labels = labels.group_by(&:title)
labels.map do |title, label| labels.map do |title, labels|
new(title, label) new(title, labels)
end end
end end
def initialize(title, labels) def initialize(title, labels)
@title = title @title = title
@labels = labels @labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end end
end end
class GlobalMilestone class GlobalMilestone
include Milestoneish
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
...@@ -28,33 +30,7 @@ class GlobalMilestone ...@@ -28,33 +30,7 @@ class GlobalMilestone
end end
def projects def projects
milestones.map { |milestone| milestone.project } @projects ||= Project.for_milestones(milestones.map(&:id))
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
end end
def state def state
...@@ -76,35 +52,20 @@ class GlobalMilestone ...@@ -76,35 +52,20 @@ class GlobalMilestone
end end
def issues def issues
@issues ||= milestones.map(&:issues).flatten.group_by(&:state) @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end end
def merge_requests 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 end
def participants def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq @participants ||= milestones.map(&:participants).flatten.compact.uniq
end end
def opened_issues def labels
issues.values_at("opened", "reopened").compact.flatten @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
end .sort_by!(&:title)
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
end end
def due_date def due_date
......
...@@ -48,10 +48,15 @@ class Label < ActiveRecord::Base ...@@ -48,10 +48,15 @@ class Label < ActiveRecord::Base
'~' '~'
end end
##
# Pattern used to extract label references from text # Pattern used to extract label references from text
#
# This pattern supports cross-project references.
#
def self.reference_pattern def self.reference_pattern
%r{ %r{
#{reference_prefix} (#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?: (?:
(?<label_id>\d+) | # Integer-based label ID, or (?<label_id>\d+) | # Integer-based label ID, or
(?<label_name> (?<label_name>
...@@ -62,24 +67,31 @@ class Label < ActiveRecord::Base ...@@ -62,24 +67,31 @@ class Label < ActiveRecord::Base
}x }x
end end
def self.link_reference_pattern
nil
end
##
# Returns the String necessary to reference this Label in Markdown # Returns the String necessary to reference this Label in Markdown
# #
# format - Symbol format to use (default: :id, optional: :name) # 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: # Examples:
# #
# Label.first.to_reference # => "~1" # Label.first.to_reference # => "~1"
# Label.first.to_reference(:name) # => "~\"bug\"" # Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
# #
# Returns a String # Returns a String
def to_reference(format = :id) #
if format == :name && !name.include?('"') def to_reference(from_project = nil, format: :id)
%(#{self.class.reference_prefix}"#{name}") format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(from_project)
project.to_reference + reference
else else
"#{self.class.reference_prefix}#{id}" reference
end end
end end
...@@ -98,4 +110,16 @@ class Label < ActiveRecord::Base ...@@ -98,4 +110,16 @@ class Label < ActiveRecord::Base
def template? def template?
template template
end 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 end
...@@ -137,9 +137,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -137,9 +137,7 @@ class MergeRequest < ActiveRecord::Base
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } 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 :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 :of_projects, ->(ids) { where(target_project_id: ids) }
scope :opened, -> { with_states(:opened, :reopened) }
scope :merged, -> { with_state(:merged) } scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) } scope :join_project, -> { joins(:target_project) }
...@@ -537,6 +535,29 @@ class MergeRequest < ActiveRecord::Base ...@@ -537,6 +535,29 @@ class MergeRequest < ActiveRecord::Base
end end
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 def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
end end
......
...@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
# Prevent store of diff if commits amount more then 500 # Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500 COMMITS_SAFE_SIZE = 100
belongs_to :merge_request belongs_to :merge_request
......
...@@ -24,12 +24,13 @@ class Milestone < ActiveRecord::Base ...@@ -24,12 +24,13 @@ class Milestone < ActiveRecord::Base
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
include Milestoneish
belongs_to :project belongs_to :project
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests 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 :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
...@@ -92,30 +93,6 @@ class Milestone < ActiveRecord::Base ...@@ -92,30 +93,6 @@ class Milestone < ActiveRecord::Base
end end
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 def expires_at
if due_date if due_date
if due_date.past? if due_date.past?
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
......
...@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base ...@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects has_many :lfs_objects, through: :lfs_objects_projects
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
...@@ -215,6 +216,7 @@ class Project < ActiveRecord::Base ...@@ -215,6 +216,7 @@ class Project < ActiveRecord::Base
scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) } scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
state_machine :import_status, initial: :none do state_machine :import_status, initial: :none do
event :import_start do event :import_start do
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
...@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet ...@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes # Scopes
scope :fresh, -> { order("created_at DESC") } 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 end
...@@ -133,18 +133,18 @@ class Repository ...@@ -133,18 +133,18 @@ class Repository
rugged.branches.create(branch_name, target) rugged.branches.create(branch_name, target)
end end
expire_branches_cache after_create_branch
find_branch(branch_name) find_branch(branch_name)
end end
def add_tag(tag_name, ref, message = nil) 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) gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end end
def rm_branch(user, branch_name) def rm_branch(user, branch_name)
expire_branches_cache before_remove_branch
branch = find_branch(branch_name) branch = find_branch(branch_name)
oldrev = branch.try(:target) oldrev = branch.try(:target)
...@@ -155,12 +155,12 @@ class Repository ...@@ -155,12 +155,12 @@ class Repository
rugged.branches.delete(branch_name) rugged.branches.delete(branch_name)
end end
expire_branches_cache after_remove_branch
true true
end end
def rm_tag(tag_name) def rm_tag(tag_name)
expire_tags_cache before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name) gitlab_shell.rm_tag(path_with_namespace, tag_name)
end end
...@@ -183,6 +183,14 @@ class Repository ...@@ -183,6 +183,14 @@ class Repository
end end
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 # Return repo size in megabytes
# Cached in redis # Cached in redis
def size def size
...@@ -278,6 +286,16 @@ class Repository ...@@ -278,6 +286,16 @@ class Repository
@has_visible_content = nil @has_visible_content = nil
end 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 def rebuild_cache
cache_keys.each do |key| cache_keys.each do |key|
cache.expire(key) cache.expire(key)
...@@ -313,9 +331,17 @@ class Repository ...@@ -313,9 +331,17 @@ class Repository
expire_root_ref_cache expire_root_ref_cache
end end
# Runs code before creating a new tag. # Runs code before pushing (= creating or removing) a tag.
def before_create_tag def before_push_tag
expire_cache 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 end
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
...@@ -330,12 +356,21 @@ class Repository ...@@ -330,12 +356,21 @@ class Repository
# Runs code after a new branch has been created. # Runs code after a new branch has been created.
def after_create_branch def after_create_branch
expire_branches_cache
expire_has_visible_content_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 end
# Runs code after an existing branch has been removed. # Runs code after an existing branch has been removed.
def after_remove_branch def after_remove_branch
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
expire_branches_cache
end end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
...@@ -812,6 +847,12 @@ class Repository ...@@ -812,6 +847,12 @@ class Repository
raw_repository.ls_files(actual_ref) raw_repository.ls_files(actual_ref)
end end
def main_language
unless empty?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end
end
private private
def cache def cache
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
...@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base ...@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") } 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 participant :author, :notes
...@@ -111,10 +108,6 @@ class Snippet < ActiveRecord::Base ...@@ -111,10 +108,6 @@ class Snippet < ActiveRecord::Base
nil nil
end end
def expired?
expires_at && expires_at < Time.current
end
def visibility_level_field def visibility_level_field
visibility_level visibility_level
end end
......
...@@ -14,6 +14,7 @@ class GitPushService < BaseService ...@@ -14,6 +14,7 @@ class GitPushService < BaseService
# 3. Recognizes cross-references from commit messages # 3. Recognizes cross-references from commit messages
# 4. Executes the project's web hooks # 4. Executes the project's web hooks
# 5. Executes the project's services # 5. Executes the project's services
# 6. Checks if the project's main language has changed
# #
def execute def execute
@project.repository.after_push_commit(branch_name) @project.repository.after_push_commit(branch_name)
...@@ -42,11 +43,24 @@ class GitPushService < BaseService ...@@ -42,11 +43,24 @@ class GitPushService < BaseService
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages process_commit_messages
end 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 # Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change. # could cause the last commit of a merge request to change.
update_merge_requests update_merge_requests
end 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 protected
def update_merge_requests def update_merge_requests
...@@ -96,9 +110,11 @@ class GitPushService < BaseService ...@@ -96,9 +110,11 @@ class GitPushService < BaseService
# a different branch. # a different branch.
closed_issues = commit.closes_issues(current_user) closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue| closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end end
end end
end
commit.create_cross_references!(authors[commit], closed_issues) commit.create_cross_references!(authors[commit], closed_issues)
end end
......
...@@ -2,7 +2,7 @@ class GitTagPushService ...@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref) def execute(project, user, oldrev, newrev, ref)
project.repository.before_create_tag project.repository.before_push_tag
@project, @user = project, user @project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref) @push_data = build_push_data(oldrev, newrev, ref)
......
...@@ -21,9 +21,11 @@ module MergeRequests ...@@ -21,9 +21,11 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user) closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue| closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
end end
end end
end
def create_merge_event(merge_request, current_user) def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user) EventCreateService.new.merge_mr(merge_request, current_user)
......
...@@ -66,7 +66,7 @@ class SystemNoteService ...@@ -66,7 +66,7 @@ class SystemNoteService
def self.change_label(noteable, project, author, added_labels, removed_labels) def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count 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(' ') added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ') removed_labels = removed_labels.map(&references).join(' ')
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.nav-controls .nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = 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" = 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? - if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do = link_to new_project_path, class: 'btn btn-new' do
= icon('plus') = 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) } = render 'shared/milestones/milestone',
.row milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
.col-sm-6 issues_path: issues_dashboard_path(milestone_title: milestone.title),
%strong merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
= link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) milestone: milestone,
.col-sm-6 dashboard: true
.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
- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path - header_title "Milestones", dashboard_milestones_path
.detail-page-header = render 'shared/milestones/top', milestone: @milestone
.status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } = render 'shared/milestones/summary', milestone: @milestone
- if @milestone.closed? = render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
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
...@@ -4,7 +4,10 @@ ...@@ -4,7 +4,10 @@
.todo-title .todo-title
%span.author-name %span.author-name
= link_to_author todo - if todo.author
= link_to_author(todo)
- else
(removed)
%span.todo-label %span.todo-label
= todo_action_name(todo) = todo_action_name(todo)
= todo_target_link(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 @@ ...@@ -10,11 +10,11 @@
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu
%li %li
= link_to explore_projects_filter_path(visibility_level: nil) do = link_to filter_projects_path(visibility_level: nil) do
Any Any
- Gitlab::VisibilityLevel.values.each do |level| - Gitlab::VisibilityLevel.values.each do |level|
%li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } %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_icon(level)
= visibility_level_label(level) = visibility_level_label(level)
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu
%li %li
= link_to explore_projects_filter_path(tag: nil) do = link_to filter_projects_path(tag: nil) do
Any Any
- @tags.each do |tag| - @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(tag: tag.name) do = link_to filter_projects_path(tag: tag.name) do
%i.fa.fa-tag = icon('tag')
= tag.name = tag.name
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- if @projects.present? - if @projects.present?
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group - if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
= icon('plus') = icon('plus')
......
%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) } = render 'shared/milestones/milestone',
.row milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title),
.col-sm-6 issues_path: issues_group_path(@group, milestone_title: milestone.title),
%strong merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
= link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) milestone: milestone
.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"
- page_title @milestone.title, "Milestones"
= render "header_title" = render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
.detail-page-header = render 'shared/milestones/summary', milestone: @milestone
.status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
- 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
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
- blob = sanitize_svg(blob) - blob = sanitize_svg(blob)
%img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
- else - 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 %div.panel.panel-default
.panel-heading .panel-heading
Commits (#{@commits.count}) Commits (#{@commits.count})
- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - if hidden > 0
%ul.well-list %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 = render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled %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 - else
%ul.well-list= render Commit.decorate(@commits, @project), project: @project %ul.well-list= render commits, project: @project
- unless defined?(project) - unless defined?(project)
- project = @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 .row.commits-row
.col-md-2.hidden-xs.hidden-sm .col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date %h5.commits-row-date
...@@ -13,3 +15,7 @@ ...@@ -13,3 +15,7 @@
%ul.bordered-list %ul.bordered-list
= render commits, project: project = render commits, project: project
%hr.lists-separator %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 @@ ...@@ -15,9 +15,9 @@
= nav_link(html_options: {class: branches_tab_class}) do = nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do = link_to namespace_project_branches_path(@project.namespace, @project) do
Branches Branches
%span.badge.js-totalbranch-count= @repository.branches.size %span.badge.js-totalbranch-count= @repository.branch_count
= nav_link(controller: [:tags, :releases]) do = nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do = link_to namespace_project_tags_path(@project.namespace, @project) do
Tags Tags
%span.badge.js-totaltags-count= @repository.tags.length %span.badge.js-totaltags-count= @repository.tag_count
...@@ -34,6 +34,8 @@ ...@@ -34,6 +34,8 @@
%span into %span into
= link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
= @merge_request.target_branch = @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/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml" = 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) } = render 'shared/milestones/milestone',
.row milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone),
.col-sm-6 issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
%strong merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
= link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) milestone: 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
...@@ -42,102 +42,9 @@ ...@@ -42,102 +42,9 @@
= preserve do = preserve do
= markdown @milestone.description = markdown @milestone.description
- if @milestone.issues.any? && @milestone.can_be_closed? - if @milestone.complete? && @milestone.active?
.alert.alert-success.prepend-top-default .alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now. %span All issues for this milestone are closed. You may close milestone now.
.context.prepend-top-default = render 'shared/milestones/summary', milestone: @milestone, project: @project
.milestone-summary = render 'shared/milestones/tabs', milestone: @milestone
%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'
-# @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 @@ ...@@ -28,6 +28,9 @@
= project.name = project.name
.controls .controls
- if project.main_language
%span
= project.main_language
- if ci_commit - if ci_commit
%span %span
= render_ci_status(ci_commit) = render_ci_status(ci_commit)
......
...@@ -7,8 +7,6 @@ Rails.application.configure do ...@@ -7,8 +7,6 @@ Rails.application.configure do
# and recreated between test runs. Don't rely on the data there! # and recreated between test runs. Don't rely on the data there!
config.cache_classes = false config.cache_classes = false
config.cache_store = :null_store
# Configure static asset server for tests with Cache-Control for performance # Configure static asset server for tests with Cache-Control for performance
config.serve_static_files = true config.serve_static_files = true
config.static_cache_control = "public, max-age=3600" 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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -697,6 +697,7 @@ ActiveRecord::Schema.define(version: 20160222153918) do ...@@ -697,6 +697,7 @@ ActiveRecord::Schema.define(version: 20160222153918) do
t.integer "build_timeout", default: 3600, null: false t.integer "build_timeout", default: 3600, null: false
t.boolean "pending_delete", default: false t.boolean "pending_delete", default: false
t.boolean "public_builds", default: true, null: false t.boolean "public_builds", default: true, null: false
t.string "main_language"
end end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree 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 ...@@ -777,7 +778,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "file_name" t.string "file_name"
t.datetime "expires_at"
t.string "type" t.string "type"
t.integer "visibility_level", default: 0, null: false t.integer "visibility_level", default: 0, null: false
end end
...@@ -785,7 +785,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do ...@@ -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", ["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", "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", ["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", ["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", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
......
...@@ -213,8 +213,7 @@ Example response: ...@@ -213,8 +213,7 @@ Example response:
## Commit status ## Commit status
Since GitLab 8.1, this is the new commit status API. The documentation in Since GitLab 8.1, this is the new commit status API.
[ci/api/commits](../ci/api/commits.md) is deprecated.
### Get the status of a commit ### Get the status of a commit
......
...@@ -145,7 +145,6 @@ Parameters: ...@@ -145,7 +145,6 @@ Parameters:
"state": "active", "state": "active",
"created_at": "2013-09-30T13:46:01Z" "created_at": "2013-09-30T13:46:01Z"
}, },
"expires_at": null,
"updated_at": "2013-10-02T07:34:20Z", "updated_at": "2013-10-02T07:34:20Z",
"created_at": "2013-10-02T07:34:20Z" "created_at": "2013-10-02T07:34:20Z"
} }
......
...@@ -51,7 +51,6 @@ Parameters: ...@@ -51,7 +51,6 @@ Parameters:
"state": "active", "state": "active",
"created_at": "2012-05-23T08:00:58Z" "created_at": "2012-05-23T08:00:58Z"
}, },
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z", "updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z" "created_at": "2012-06-28T10:52:04Z"
} }
......
...@@ -151,6 +151,8 @@ Parameters: ...@@ -151,6 +151,8 @@ Parameters:
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"created_at": "2012-05-23T08:00:58Z", "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, "bio": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
......
# GitLab CI API # GitLab CI API
## Resources ## Purpose
- [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/
If preferred, you may instead send the `private-token` as a header in Main purpose of GitLab CI API is to provide necessary data and context for
your request: 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. You need to prepend this prefix to all examples in this documentation, like:
It can be used to get project commits and builds information.
You can use project token only for certain project.
### 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. ## Resources
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:
- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON - [Builds](builds.md)
- `201 Created` - The `POST` request was successful and the resource is returned as JSON - [Runners](runners.md)
- `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 API # 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 ## Builds
### Runs oldest pending build by runner ### Runs oldest pending build by runner
POST /ci/builds/register POST /ci/api/v1/builds/register
Parameters: Parameters:
* `token` (required) - The unique token of runner * `token` (required) - Unique runner token
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
}
}
]
}
```
### Update details of an existing build ### Update details of an existing build
PUT /ci/builds/:id PUT /ci/api/v1/builds/:id
Parameters: Parameters:
* `id` (required) - The ID of a project * `id` (required) - The ID of a project
* `token` (required) - Unique runner token
* `state` (optional) - The state of a build * `state` (optional) - The state of a build
* `trace` (optional) - The trace 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 # 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 _**Note:** This API is intended to be used only by Runners as their own
communication channel. For the consumer API see the communication channel. For the consumer API see the
[new Runners API](../../api/runners.md)._ [new Runners API](../../api/runners.md)._
## Runners ## Authentication
### Retrieve all runners
__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 1. Unique runner's token
instance.
GET /ci/runners Token assigned to runner after it has been registered.
Returns: 2. Using runners' registration token
```json This is a token that can be found in project's settings.
[ It can be also found in Admin area &raquo; Runners settings.
{
"id" : 85,
"token" : "12b68e90394084703135"
},
{
"id" : 86,
"token" : "76bf894e969364709864"
},
]
```
### 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. Used to make GitLab CI aware of available runners.
POST /ci/runners/register POST /ci/api/v1/runners/register
Parameters: 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 ### Delete a runner
Used to remove runner.
__Authentication is done by runner token__ DELETE /ci/api/v1/runners/delete
Used to removing runners.
DELETE /ci/runners/delete
Parameters: Parameters:
* `token` (required) - The runner token. * `token` (required) - Unique 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"
}
```
...@@ -9,4 +9,5 @@ ...@@ -9,4 +9,5 @@
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md) - [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 - [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 @@ ...@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab **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. 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. Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update. 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 ...@@ -467,12 +467,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj
### Initial Login ### 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 If you didn't [provide a root password during setup](#initialize-database-and-activate-advanced-features),
5iveL!fe 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!** **Enjoy!**
......
...@@ -39,3 +39,34 @@ please see the [project_services directory][projects-code]. ...@@ -39,3 +39,34 @@ please see the [project_services directory][projects-code].
[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html [jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
[Project Service]: ../project_services/project_services.md [Project Service]: ../project_services/project_services.md
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services [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 ...@@ -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 `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 before any LDAP-protocol data is exchanged but no validation of
the LDAP server's SSL certificate is performed. 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. ...@@ -133,12 +133,16 @@ will be returned to GitLab and will be signed in.
## Troubleshooting ## Troubleshooting
### 500 error after login
If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, 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. 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 Make sure the IdP provides a claim containing the user's email address, using claim name
`email` or `mail`. `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 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 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 message `Can't verify CSRF token authenticity`. This means that there is an error during
...@@ -148,3 +152,35 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th ...@@ -148,3 +152,35 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th
`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, `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 where it can then be seen in the usual logs, or as a flash message in the login
screen. 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 @@ ...@@ -2,19 +2,11 @@
## On Slack ## 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. [Sign in to Slack](https://slack.com/signin)
1. Select **Apps & Custom Integrations** from the dropdown next to your team name. 1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
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. Choose the channel name you want to send notifications to. 1. Choose the channel name you want to send notifications to.
......
...@@ -207,6 +207,7 @@ GFM also recognizes certain cross-project references: ...@@ -207,6 +207,7 @@ GFM also recognizes certain cross-project references:
| `namespace/project$123` | snippet | | `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison | | `namespace/project@9ba12248...b19a04f5` | commit range comparison |
| `namespace/project~"Some label"` | issues with given label |
## Task Lists ## Task Lists
......
...@@ -219,3 +219,16 @@ You can see from the above image that there are four references to GitLab: ...@@ -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 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" [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" [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 ...@@ -64,6 +64,9 @@ sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql') # PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment 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 # Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production 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 ...@@ -62,7 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment
# MySQL # MySQL
sudo -u git -H bundle install --without development test postgres --deployment 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 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 sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
``` ```
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome. 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. GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version.
......
...@@ -582,7 +582,6 @@ X-Gitlab-Event: Note Hook ...@@ -582,7 +582,6 @@ X-Gitlab-Event: Note Hook
"created_at": "2015-04-09 02:40:38 UTC", "created_at": "2015-04-09 02:40:38 UTC",
"updated_at": "2015-04-09 02:40:38 UTC", "updated_at": "2015-04-09 02:40:38 UTC",
"file_name": "test.rb", "file_name": "test.rb",
"expires_at": null,
"type": "ProjectSnippet", "type": "ProjectSnippet",
"visibility_level": 0 "visibility_level": 0
} }
......
...@@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with ...@@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with
## Configuration ## 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: There are two configuration options to help GitLab server administrators:
...@@ -37,5 +38,8 @@ In `config/gitlab.yml`: ...@@ -37,5 +38,8 @@ In `config/gitlab.yml`:
## Known limitations ## 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 * 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 # Git LFS
Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. Managing large files such as audio, video and graphics files has always been one
The general recommendation is to not have Git repositories larger than 1GB to preserve performance. 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 GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
environments it is not always convenient to use different commands to differentiate between the large files and regular ones. (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 ## How it works
Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests. Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file. 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 ## GitLab server configuration
...@@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do ...@@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do
## Known limitations ## 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 * 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 * Any Git LFS request will ask for HTTPS credentials to be provided so good Git
* 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) 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 ## Using Git LFS
Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS: Lets take a look at the workflow when you need to check large files into your Git
For example, if you want to upload a very large file and check it into your Git repository: repository with Git LFS. For example, if you want to upload a very large file and
check it into your Git repository:
```bash ```bash
git clone git@gitlab.example.com:group/project.git git clone git@gitlab.example.com:group/project.git
...@@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project ...@@ -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 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 ```bash
cp ~/tmp/debian.iso ./ # copy a large file into the current directory 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 ...@@ -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 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 ```bash
git clone git@gitlab.example.com:group/project.git 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 ```bash
git lfs fetch master git lfs fetch master
...@@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project. ...@@ -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 * 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. LFS object you are trying to push to the project or fetch from the project is not
Probably the object was removed from the server. available to the project anymore. Probably the object was removed from the server.
* Local git repository is using deprecated LFS API * Local git repository is using deprecated LFS API
...@@ -89,16 +102,26 @@ git lfs logs last ...@@ -89,16 +102,26 @@ git lfs logs last
If the status `error 501` is shown, it is because: 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 ### 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`, If you push a LFS object to a project and you receive an error similar to:
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP. `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: 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 ...@@ -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 ### 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 ```bash
git config --global credential.helper 'cache --timeout=3600' 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). More details about various methods of storing the user credentials can be found
\ No newline at end of file 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 ...@@ -10,3 +10,8 @@ Feature: Dashboard Archived Projects
Scenario: I should see non-archived projects on dashboard Scenario: I should see non-archived projects on dashboard
Then I should see "Shop" project link Then I should see "Shop" project link
And I should not see "Forum" 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 ...@@ -140,4 +140,4 @@ Feature: Explore Projects
When I visit the explore starred projects When I visit the explore starred projects
Then I should see project "Community" Then I should see project "Community"
And I should see project "Internal" 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 ...@@ -28,3 +28,20 @@ Feature: Group Milestones
And I fill milestone name And I fill milestone name
When I press create mileston button When I press create mileston button
Then milestone in each project should be created 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 ...@@ -58,14 +58,6 @@ Feature: Project Issues
Then I should see comment "XML attached" Then I should see comment "XML attached"
And I should see an error alert section within the comment form 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 @javascript
Scenario: Visiting Issues after being sorted the list Scenario: Visiting Issues after being sorted the list
Given I visit project "Shop" issues page Given I visit project "Shop" issues page
......
...@@ -26,6 +26,16 @@ Feature: Project Merge Requests ...@@ -26,6 +26,16 @@ Feature: Project Merge Requests
When I visit project "Shop" merge requests page When I visit project "Shop" merge requests page
Then I should see "other_branch" branch 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 Scenario: I should see rejected merge requests
Given I click link "Closed" Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests Then I should see "Feature NS-03" in merge requests
...@@ -76,15 +86,6 @@ Feature: Project Merge Requests ...@@ -76,15 +86,6 @@ Feature: Project Merge Requests
And I leave a comment like "XML attached" And I leave a comment like "XML attached"
Then I should see comment "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 @javascript
Scenario: Visiting Merge Requests after being sorted the list Scenario: Visiting Merge Requests after being sorted the list
Given I visit project "Shop" merge requests page Given I visit project "Shop" merge requests page
...@@ -118,16 +119,6 @@ Feature: Project Merge Requests ...@@ -118,16 +119,6 @@ Feature: Project Merge Requests
And I sort the list by "Least popular" And I sort the list by "Least popular"
Then The list should be sorted 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 @javascript
Scenario: I comment on a merge request diff Scenario: I comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside Given project "Shop" have "Bug NS-05" open merge request with diffs inside
......
...@@ -19,4 +19,8 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps ...@@ -19,4 +19,8 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps
step 'I should see "Forum" project link' do step 'I should see "Forum" project link' do
expect(page).to have_link "Forum" expect(page).to have_link "Forum"
end end
step 'I click "Show archived projects" link' do
click_link "Show archived projects"
end
end end
...@@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end end
step 'I click on one group milestone' do 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' click_link 'GL-113'
end end
...@@ -33,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -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 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('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 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)) expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
end end
...@@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end end
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 private
def group_milestone def group_milestone
...@@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
project = create :project, path: path, group: group project = create :project, path: path, group: group
milestone = create :milestone, title: "Version 7.2", project: project milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
create(:label, project: project, title: 'feature')
create :issue, create :issue,
project: project, project: project,
assignee: current_user, assignee: current_user,
...@@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
due_date: '2114-08-20', due_date: '2114-08-20',
description: 'Lorem Ipsum is simply dummy text' description: 'Lorem Ipsum is simply dummy text'
create :issue, issue = create :issue,
project: project, project: project,
assignee: current_user, assignee: current_user,
author: current_user, author: current_user,
milestone: milestone milestone: milestone
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end end
end end
end end
...@@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
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) def filter_issue(text)
fill_in 'issue_search', with: text fill_in 'issue_search', with: text
end end
......
...@@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps ...@@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end end
step 'I should see 3 issues' do 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 end
step 'I click link to remove milestone' do step 'I click link to remove milestone' do
......
...@@ -60,7 +60,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -60,7 +60,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).not_to have_content "Feature NS-03" expect(page).not_to have_content "Feature NS-03"
end end
step 'I should not see "Bug NS-04" in merge requests' do step 'I should not see "Bug NS-04" in merge requests' do
expect(page).not_to have_content "Bug NS-04" expect(page).not_to have_content "Bug NS-04"
end end
...@@ -121,6 +120,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -121,6 +120,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first) author: project.users.first)
end 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 step 'project "Shop" have "Feature NS-03" closed merge request' do
create(:closed_merge_request, create(:closed_merge_request,
title: "Feature NS-03", title: "Feature NS-03",
...@@ -490,12 +505,16 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -490,12 +505,16 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end end
end end
step 'I should see "Bug NS-05" at the top' do step 'I should see the diverged commits count' do
expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05") page.within ".mr-source-target" do
expect(page).to have_content /([0-9]+ commits behind)/
end
end end
step 'I should see "Bug NS-04" at the top' do step 'I should not see the diverged commits count' do
expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04") page.within ".mr-source-target" do
expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
end
end end
def merge_request def merge_request
......
...@@ -29,7 +29,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps ...@@ -29,7 +29,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'There is an open Merge Request' do step 'There is an open Merge Request' do
@user = create(:user) @user = create(:user)
@project = create(:project, :public) @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) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end end
......
...@@ -36,7 +36,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps ...@@ -36,7 +36,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'There is an open Merge Request' do step 'There is an open Merge Request' do
@user = create(:user) @user = create(:user)
@project = create(:project, :public) @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) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end end
......
...@@ -144,11 +144,4 @@ module SharedNote ...@@ -144,11 +144,4 @@ module SharedNote
expect(page).to have_content("+1 Awesome!") expect(page).to have_content("+1 Awesome!")
end end
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 end
...@@ -388,13 +388,19 @@ module SharedPaths ...@@ -388,13 +388,19 @@ module SharedPaths
end end
step 'I visit merge request page "Bug NS-04"' do step 'I visit merge request page "Bug NS-04"' do
mr = MergeRequest.find_by(title: "Bug NS-04") visit merge_request_path("Bug NS-04")
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
end end
step 'I visit merge request page "Bug NS-05"' do step 'I visit merge request page "Bug NS-05"' do
mr = MergeRequest.find_by(title: "Bug NS-05") visit merge_request_path("Bug NS-05")
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) 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 end
step 'I visit merge request page "Bug CO-01"' do step 'I visit merge request page "Bug CO-01"' do
...@@ -503,6 +509,11 @@ module SharedPaths ...@@ -503,6 +509,11 @@ module SharedPaths
Project.find_by!(name: 'Shop') Project.find_by!(name: 'Shop')
end 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 # Errors
# ---------------------------------------- # ----------------------------------------
......
...@@ -14,6 +14,7 @@ require 'sidekiq/testing/inline' ...@@ -14,6 +14,7 @@ require 'sidekiq/testing/inline'
require_relative 'capybara' require_relative 'capybara'
require_relative 'db_cleaner' require_relative 'db_cleaner'
require_relative 'rerun'
%w(select2_helper test_env repo_helpers).each do |f| %w(select2_helper test_env repo_helpers).each do |f|
require Rails.root.join('spec', 'support', 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 ...@@ -23,6 +23,8 @@ module API
end end
class UserFull < User class UserFull < User
expose :last_sign_in_at
expose :confirmed_at
expose :email expose :email
expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity expose :identities, using: Entities::Identity
...@@ -141,7 +143,7 @@ module API ...@@ -141,7 +143,7 @@ module API
class ProjectSnippet < Grape::Entity class ProjectSnippet < Grape::Entity
expose :id, :title, :file_name expose :id, :title, :file_name
expose :author, using: Entities::UserBasic expose :author, using: Entities::UserBasic
expose :expires_at, :updated_at, :created_at expose :updated_at, :created_at
end end
class ProjectEntity < Grape::Entity class ProjectEntity < Grape::Entity
......
...@@ -94,6 +94,8 @@ module Banzai ...@@ -94,6 +94,8 @@ module Banzai
object_link_filter(link, object_class.link_reference_pattern, link_text: text) object_link_filter(link, object_class.link_reference_pattern, link_text: text)
end end
end end
doc
end end
# Replace references (like `!123` for merge requests) in text with links # Replace references (like `!123` for merge requests) in text with links
......
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces label references with links. # HTML filter that replaces label references with links.
class LabelReferenceFilter < ReferenceFilter class LabelReferenceFilter < AbstractReferenceFilter
# Public: Find label references in text def self.object_class
# Label
# 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
end
def self.referenced_by(node)
{ label: LazyReference.new(Label, node.attr("data-label")) }
end end
def call def find_object(project, id)
replace_text_nodes_matching(Label.reference_pattern) do |content| project.labels.find(id)
label_link_filter(content)
end end
replace_link_nodes_with_href(Label.reference_pattern) do |link, text| def self.references_in(text, pattern = Label.reference_pattern)
label_link_filter(link, link_text: text) text.gsub(pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~
end end
end end
# Replace label references in text with links to the label specified. def references_in(text, pattern = Label.reference_pattern)
# text.gsub(pattern) do |match|
# text - String text to replace references in. project = project_from_ref($~[:project])
# params = label_params($~[:label_id].to_i, $~[:label_name])
# Returns a String with label references replaced with links. All links label = project.labels.find_by(params)
# 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
)
text = link_text || render_colored_label(label) if label
yield match, label.id, $~[:project], $~
%(<a href="#{url}" #{data}
class="#{klass}">#{escape_once(text)}</a>)
else else
match match
end end
end end
end end
def url_for_label(project, label) def url_for_object(label, project)
h = Gitlab::Application.routes.url_helpers h = Gitlab::Application.routes.url_helpers
h.namespace_project_issues_url( project.namespace, project, label_name: label.name, h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
only_path: context[:only_path]) only_path: context[:only_path])
end end
def render_colored_label(label) def object_link_text(object, matches)
LabelsHelper.render_colored_label(label) if context[:project] == object.project
LabelsHelper.render_colored_label(object)
else
LabelsHelper.render_colored_cross_project_label(object)
end
end end
# Parameters to pass to `Label.find_by` based on the given arguments # Parameters to pass to `Label.find_by` based on the given arguments
......
...@@ -76,7 +76,7 @@ module Gitlab ...@@ -76,7 +76,7 @@ module Gitlab
project.issues.create!( project.issues.create!(
description: body, description: body,
title: issue["title"], 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) author_id: gl_user_id(project, reporter)
) )
end 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 ...@@ -4,53 +4,59 @@ namespace :spinach do
namespace :project do namespace :project do
desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features"
task :half do task :half do
cmds = [ run_spinach_tests('@project_commits,@project_issues,@project_merge_requests')
%W(rake gitlab:setup),
%W(spinach --tags @project_commits,@project_issues,@project_merge_requests),
]
run_commands(cmds)
end end
desc "GitLab | Spinach | Run remaining project spinach features" desc "GitLab | Spinach | Run remaining project spinach features"
task :rest do task :rest do
cmds = [ run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests')
%W(rake gitlab:setup),
%W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests),
]
run_commands(cmds)
end end
end end
desc "GitLab | Spinach | Run project spinach features" desc "GitLab | Spinach | Run project spinach features"
task :project do task :project do
cmds = [ run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets')
%W(rake gitlab:setup),
%W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets),
]
run_commands(cmds)
end end
desc "GitLab | Spinach | Run other spinach features" desc "GitLab | Spinach | Run other spinach features"
task :other do task :other do
cmds = [ run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets')
%W(rake gitlab:setup), end
%W(spinach --tags @admin,@dashboard,@profile,@public,@snippets),
] desc "GitLab | Spinach | Run other spinach features"
run_commands(cmds) task :builds do
run_spinach_tests('@builds')
end end
end end
desc "GitLab | Run spinach" desc "GitLab | Run spinach"
task :spinach do task :spinach do
cmds = [ run_spinach_tests(nil)
%W(rake gitlab:setup), end
%W(spinach),
] def run_command(cmd)
run_commands(cmds) system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
end end
def run_commands(cmds) def run_spinach_command(args)
cmds.each do |cmd| run_command(%w(spinach -r rerun) + args)
system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") 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 end
raise("spinach tests for #{tags} failed!") unless success
end end
#!/bin/bash #!/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 if [ -f /.dockerinit ]; then
mkdir -p vendor 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 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 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 # Try to install packages
apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ 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 libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
cp config/database.yml.mysql config/database.yml cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml
...@@ -20,7 +34,7 @@ if [ -f /.dockerinit ]; then ...@@ -20,7 +34,7 @@ if [ -f /.dockerinit ]; then
cp config/resque.yml.example config/resque.yml cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml
export FLAGS=(--path vendor) export FLAGS=(--path vendor --retry 3)
else else
export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
cp config/database.yml.mysql config/database.yml cp config/database.yml.mysql config/database.yml
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::ForksController do describe Projects::ForksController do
let(:user) { create(:user) } 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(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) } let(:group) { create(:group, owner: forked_project.creator) }
......
FactoryGirl.define do FactoryGirl.define do
factory :email do factory :email do
user user
email do email { FFaker::Internet.email('alias') }
FFaker::Internet.email('alias')
end
factory :another_email do
email do
FFaker::Internet.email('another.alias')
end
end
end end
end end
...@@ -69,6 +69,16 @@ FactoryGirl.define do ...@@ -69,6 +69,16 @@ FactoryGirl.define do
target_branch "master" target_branch "master"
end 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 trait :merge_when_build_succeeds do
merge_when_build_succeeds true merge_when_build_succeeds true
merge_user author merge_user author
......
FactoryGirl.define do FactoryGirl.define do
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet 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
end end
...@@ -2,6 +2,26 @@ FactoryGirl.define do ...@@ -2,6 +2,26 @@ FactoryGirl.define do
factory :project_member do factory :project_member do
user user
project 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
end end
FactoryGirl.define do FactoryGirl.define do
factory :project_snippet do factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project project
author
title
content
file_name
end end
end end
FactoryGirl.define do FactoryGirl.define do
factory :service do factory :service do
type ""
title "GitLab CI"
project project
end end
end end
...@@ -12,5 +12,17 @@ FactoryGirl.define do ...@@ -12,5 +12,17 @@ FactoryGirl.define do
title title
content content
file_name 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
end end
...@@ -8,12 +8,11 @@ describe SnippetsFinder do ...@@ -8,12 +8,11 @@ describe SnippetsFinder do
let(:project1) { create(:empty_project, :public, group: group) } let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) } let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do context ':all filter' do
before do before do
@snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) @snippet1 = create(:personal_snippet, :private)
@snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) @snippet2 = create(:personal_snippet, :internal)
@snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) @snippet3 = create(:personal_snippet, :public)
end end
it "returns all private and internal snippets" do it "returns all private and internal snippets" do
...@@ -31,9 +30,9 @@ describe SnippetsFinder do ...@@ -31,9 +30,9 @@ describe SnippetsFinder do
context ':by_user filter' do context ':by_user filter' do
before do before do
@snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) @snippet1 = create(:personal_snippet, :private, author: user)
@snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) @snippet2 = create(:personal_snippet, :internal, author: user)
@snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) @snippet3 = create(:personal_snippet, :public, author: user)
end end
it "returns all public and internal snippets" do it "returns all public and internal snippets" do
...@@ -75,9 +74,9 @@ describe SnippetsFinder do ...@@ -75,9 +74,9 @@ describe SnippetsFinder do
context 'by_project filter' do context 'by_project filter' do
before do before do
@snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) @snippet1 = create(:project_snippet, :private, project: project1)
@snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) @snippet2 = create(:project_snippet, :internal, project: project1)
@snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) @snippet3 = create(:project_snippet, :public, project: project1)
end end
it "returns public snippets for unauthorized user" do it "returns public snippets for unauthorized user" do
......
...@@ -209,7 +209,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e ...@@ -209,7 +209,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by ID: <%= simple_label.to_reference %> - Label by ID: <%= simple_label.to_reference %>
- Label by name: <%= Label.reference_prefix %><%= simple_label.name %> - 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 code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
- Link to label by reference: [Label](<%= label.to_reference %>) - Link to label by reference: [Label](<%= label.to_reference %>)
......
...@@ -58,7 +58,7 @@ describe VisibilityLevelHelper do ...@@ -58,7 +58,7 @@ describe VisibilityLevelHelper do
describe "skip_level?" do describe "skip_level?" do
describe "forks" 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) } let(:fork_project) { create(:forked_project_with_submodules) }
before do before do
...@@ -74,7 +74,7 @@ describe VisibilityLevelHelper do ...@@ -74,7 +74,7 @@ describe VisibilityLevelHelper do
end end
describe "non-forked project" do describe "non-forked project" do
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } let(:project) { create(:project, :internal) }
it "skips levels" do it "skips levels" do
expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
...@@ -84,7 +84,7 @@ describe VisibilityLevelHelper do ...@@ -84,7 +84,7 @@ describe VisibilityLevelHelper do
end end
describe "Snippet" do describe "Snippet" do
let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } let(:snippet) { create(:snippet, :internal) }
it "skips levels" do it "skips levels" do
expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
......
...@@ -111,7 +111,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -111,7 +111,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
context 'String-based multi-word references in quotes' do context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) } 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 it 'links to a valid reference' do
doc = reference_filter("See #{reference}") doc = reference_filter("See #{reference}")
...@@ -176,4 +176,29 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -176,4 +176,29 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(result[:references][:label]).to eq [label] expect(result[:references][:label]).to eq [label]
end end
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 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 ...@@ -59,18 +59,42 @@ describe Label, models: true do
context 'using id' do context 'using id' do
it 'returns a String reference to the object' do it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}" expect(label.to_reference).to eq "~#{label.id}"
expect(label.to_reference(double('project'))).to eq "~#{label.id}"
end end
end end
context 'using name' do context 'using name' do
it 'returns a String reference to the object' 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 end
it 'uses id when name contains double quote' do it 'uses id when name contains double quote' do
label = create(:label, name: %q{"irony"}) 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 end
end end
......
...@@ -274,6 +274,70 @@ describe MergeRequest, models: true do ...@@ -274,6 +274,70 @@ describe MergeRequest, models: true do
end end
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 it_behaves_like 'an editable mentionable' do
subject { create(:merge_request) } subject { create(:merge_request) }
......
...@@ -60,7 +60,7 @@ describe Milestone, models: true do ...@@ -60,7 +60,7 @@ describe Milestone, models: true do
end end
it "should recover from dividing by zero" do 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) expect(milestone.percent_complete).to eq(0)
end end
end end
...@@ -114,7 +114,6 @@ describe Milestone, models: true do ...@@ -114,7 +114,6 @@ describe Milestone, models: true do
end end
it { expect(milestone.closed_items_count).to eq(1) } 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.total_items_count).to eq(3) }
it { expect(milestone.is_empty?).to be_falsey } it { expect(milestone.is_empty?).to be_falsey }
end end
......
...@@ -24,7 +24,7 @@ require 'spec_helper' ...@@ -24,7 +24,7 @@ require 'spec_helper'
describe Note, models: true do describe Note, models: true do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } 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 belong_to(:author).class_name('User') }
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
......
...@@ -68,6 +68,7 @@ describe Project, models: true do ...@@ -68,6 +68,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) } it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
end end
describe 'modules' do describe 'modules' do
...@@ -561,7 +562,7 @@ describe Project, models: true do ...@@ -561,7 +562,7 @@ describe Project, models: true do
end end
describe '#visibility_level_allowed?' do 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 context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
......
...@@ -148,6 +148,12 @@ describe Repository, models: true do ...@@ -148,6 +148,12 @@ describe Repository, models: true do
expect(branch.name).to eq('new_feature') expect(branch.name).to eq('new_feature')
end 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 end
context 'when pre hooks failed' do context 'when pre hooks failed' do
...@@ -405,7 +411,7 @@ describe Repository, models: true do ...@@ -405,7 +411,7 @@ describe Repository, models: true do
end end
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 # This method is private but we need it for testing purposes. Sadly there's
# no other proper way of testing caching operations. # no other proper way of testing caching operations.
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
...@@ -556,11 +562,12 @@ describe Repository, models: true do ...@@ -556,11 +562,12 @@ describe Repository, models: true do
end end
end end
describe '#before_create_tag' do describe '#before_push_tag' do
it 'flushes the cache' do it 'flushes the cache' do
expect(repository).to receive(:expire_cache) expect(repository).to receive(:expire_cache)
expect(repository).to receive(:expire_tag_count_cache)
repository.before_create_tag repository.before_push_tag
end end
end end
...@@ -595,4 +602,89 @@ describe Repository, models: true do ...@@ -595,4 +602,89 @@ describe Repository, models: true do
repository.after_remove_branch repository.after_remove_branch
end end
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 end
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime
# type :string(255) # type :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# #
......
...@@ -7,8 +7,8 @@ describe API::API, api: true do ...@@ -7,8 +7,8 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' } let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
......
...@@ -7,8 +7,8 @@ describe API::API, api: true do ...@@ -7,8 +7,8 @@ describe API::API, api: true do
let(:api_user) { user } let(:api_user) { user }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) } let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) } let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
let(:commit) { create(:ci_commit, project: project)} let(:commit) { create(:ci_commit, project: project)}
let(:build) { create(:ci_build, commit: commit) } let(:build) { create(:ci_build, commit: commit) }
......
...@@ -6,9 +6,9 @@ describe API::CommitStatus, api: true do ...@@ -6,9 +6,9 @@ describe API::CommitStatus, api: true do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let(:commit) { project.repository.commit } let(:commit) { project.repository.commit }
let(:commit_status) { create(:commit_status, commit: ci_commit) } let(:commit_status) { create(:commit_status, commit: ci_commit) }
let(:guest) { create_user(ProjectMember::GUEST) } let(:guest) { create_user(:guest) }
let(:reporter) { create_user(ProjectMember::REPORTER) } let(:reporter) { create_user(:reporter) }
let(:developer) { create_user(ProjectMember::DEVELOPER) } let(:developer) { create_user(:developer) }
let(:sha) { commit.id } let(:sha) { commit.id }
...@@ -201,9 +201,9 @@ describe API::CommitStatus, api: true do ...@@ -201,9 +201,9 @@ describe API::CommitStatus, api: true do
end end
end end
def create_user(access_level) def create_user(access_level_trait)
user = create(:user) 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 user
end end
end end
...@@ -6,8 +6,8 @@ describe API::API, api: true do ...@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } 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!(: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') } 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 ...@@ -12,7 +12,7 @@ describe API::API, api: true do
end end
let(:project_user2) do let(:project_user2) do
create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) create(:project_member, :guest, user: user2, project: project)
end end
describe 'POST /projects/fork/:id' do describe 'POST /projects/fork/:id' do
......
...@@ -6,8 +6,8 @@ describe API::API, api: true do ...@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } 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_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
describe "GET /projects/:id/members" do describe "GET /projects/:id/members" do
before { project_member } before { project_member }
......
...@@ -12,8 +12,8 @@ describe API::API, api: true do ...@@ -12,8 +12,8 @@ describe API::API, api: true do
let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } 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(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } 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_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) } let(:user4) { create(:user) }
let(:project3) do let(:project3) do
create(:project, create(:project,
......
...@@ -9,8 +9,8 @@ describe API::API, api: true do ...@@ -9,8 +9,8 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tree" do describe "GET /projects/:id/repository/tree" do
context "authorized user" do context "authorized user" do
......
...@@ -28,9 +28,9 @@ describe API::Runners, api: true do ...@@ -28,9 +28,9 @@ describe API::Runners, api: true do
before do before do
# Set project access for users # Set project access for users
create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) create(:project_member, :master, user: user, project: project)
create(:project_member, user: user, project: project2, access_level: ProjectMember::MASTER) create(:project_member, :master, user: user, project: project2)
create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) create(:project_member, :reporter, user: user2, project: project)
end end
describe 'GET /runners' do describe 'GET /runners' do
......
...@@ -8,8 +8,8 @@ describe API::API, api: true do ...@@ -8,8 +8,8 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tags" do describe "GET /projects/:id/repository/tags" do
let(:tag_name) { project.repository.tag_names.sort.reverse.first } let(:tag_name) { project.repository.tag_names.sort.reverse.first }
......
...@@ -8,8 +8,8 @@ describe API::API do ...@@ -8,8 +8,8 @@ describe API::API do
let!(:trigger_token) { 'secure_token' } let!(:trigger_token) { 'secure_token' }
let!(:trigger_token_2) { 'secure_token_2' } let!(:trigger_token_2) { 'secure_token_2' }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } 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') } 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 ...@@ -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 'identities'
expect(json_response.first.keys).to include 'can_create_project' 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 '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 end
end end
......
...@@ -6,8 +6,8 @@ describe API::API, api: true do ...@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:variable) { create(:ci_variable, project: project) } let!(:variable) { create(:ci_variable, project: project) }
describe 'GET /projects/:id/variables' do 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 ...@@ -155,6 +155,23 @@ describe GitPushService, services: true do
end end
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 describe "Web Hooks" do
context "execute web hooks" do context "execute web hooks" do
it "when pushing a branch for the first time" do it "when pushing a branch for the first time" do
...@@ -254,22 +271,24 @@ describe GitPushService, services: true do ...@@ -254,22 +271,24 @@ describe GitPushService, services: true do
allow(project.repository).to receive(:commits_between). allow(project.repository).to receive(:commits_between).
and_return([closing_commit]) and_return([closing_commit])
project.team << [commit_author, :master]
end end
context "to default branches" do context "to default branches" do
it "closes issues" 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 expect(Issue.find(issue.id)).to be_closed
end end
it "adds a note indicating that the issue is now closed" do 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) 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 end
it "doesn't create additional cross-reference notes" do it "doesn't create additional cross-reference notes" do
expect(SystemNoteService).not_to receive(:cross_reference) expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, user, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
end end
it "doesn't close issues when external issue tracker is in use" do it "doesn't close issues when external issue tracker is in use" do
...@@ -277,7 +296,7 @@ describe GitPushService, services: true do ...@@ -277,7 +296,7 @@ describe GitPushService, services: true do
# The push still shouldn't create cross-reference notes. # The push still shouldn't create cross-reference notes.
expect do 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.not_to change { Note.where(project_id: project.id, system: true).count }
end end
end end
...@@ -299,7 +318,6 @@ describe GitPushService, services: true do ...@@ -299,7 +318,6 @@ describe GitPushService, services: true do
end end
end end
# EE-only tests
context "for jira issue tracker" do context "for jira issue tracker" do
include JiraServiceHelper include JiraServiceHelper
...@@ -349,7 +367,7 @@ describe GitPushService, services: true do ...@@ -349,7 +367,7 @@ describe GitPushService, services: true do
} }
}.to_json }.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( expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
body: transition_body body: transition_body
).once ).once
...@@ -360,7 +378,7 @@ describe GitPushService, services: true do ...@@ -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}]." body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json }.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( expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: comment_body body: comment_body
).once ).once
......
...@@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do ...@@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do
describe :visibility_level do describe :visibility_level do
let(:user) { create :user, admin: true } let(:user) { create :user, admin: true }
let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } let(:project) { create(:project, :internal) }
let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL } let(:forked_project) { create(:forked_project_with_submodules, :internal) }
let(:opts) { {} } let(:opts) { {} }
before do before do
......
...@@ -15,6 +15,7 @@ require 'rspec/rails' ...@@ -15,6 +15,7 @@ require 'rspec/rails'
require 'shoulda/matchers' require 'shoulda/matchers'
require 'sidekiq/testing/inline' require 'sidekiq/testing/inline'
require 'benchmark/ips' require 'benchmark/ips'
require 'rspec/retry'
# Requires supporting ruby files with custom matchers and macros, etc, # Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories. # in spec/support/ and its subdirectories.
...@@ -25,6 +26,9 @@ RSpec.configure do |config| ...@@ -25,6 +26,9 @@ RSpec.configure do |config|
config.use_instantiated_fixtures = false config.use_instantiated_fixtures = false
config.mock_with :rspec config.mock_with :rspec
config.verbose_retry = true
config.display_try_failure_messages = true
config.include Devise::TestHelpers, type: :controller config.include Devise::TestHelpers, type: :controller
config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :feature
config.include LoginHelpers, type: :request 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