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')
...@@ -112,7 +112,8 @@ class @Milestone ...@@ -112,7 +112,8 @@ class @Milestone
$(previousTabClass).hide() $(previousTabClass).hide()
$(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,7 +110,9 @@ class GitPushService < BaseService ...@@ -96,7 +110,9 @@ 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|
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end
end end
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,7 +21,9 @@ module MergeRequests ...@@ -21,7 +21,9 @@ 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|
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
end
end end
end end
......
...@@ -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,9 +3,10 @@ ...@@ -3,9 +3,10 @@
= 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
- if can? current_user, :create_projects, @group = render 'shared/projects/dropdown'
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do - if can? current_user, :create_projects, @group
= icon('plus') = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
New Project = icon('plus')
New Project
= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true = render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
%span.milestone-row
- project = issue.project
%strong #{project.name} &middot;
= link_to [project.namespace.becomes(Namespace), project, issue] do
%span.cgray ##{issue.iid}
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
= image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: ''
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list issues-sortable-list" }
- if issues
- issues.each do |issue|
= render 'issue', issue: issue
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
%span.milestone-row
- project = merge_request.project
%strong #{project.name} &middot;
= link_to [project.namespace.becomes(Namespace), project, merge_request] do
%span.cgray ##{merge_request.iid}
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
= image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list merge_requests-sortable-list" }
- if merge_requests
- merge_requests.each do |merge_request|
= render 'merge_request', merge_request: merge_request
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } = 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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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