Commit 2b3dc28d authored by Valery Sizov's avatar Valery Sizov

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

parents 43031a15 e8cd04e8
...@@ -3,17 +3,24 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -3,17 +3,24 @@ 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) - 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 - 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
- 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)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
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)
......
...@@ -23,7 +23,7 @@ class Dispatcher ...@@ -23,7 +23,7 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
when 'projects:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
......
...@@ -69,7 +69,7 @@ class @Milestone ...@@ -69,7 +69,7 @@ class @Milestone
@bindIssuesSorting() @bindIssuesSorting()
@bindMergeRequestSorting() @bindMergeRequestSorting()
@bindTabsSwitching @bindTabsSwitching()
bindIssuesSorting: -> bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
...@@ -104,7 +104,7 @@ class @Milestone ...@@ -104,7 +104,7 @@ class @Milestone
).disableSelection() ).disableSelection()
bindMergeRequestSorting: -> bindTabsSwitching: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show') currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show') previousTabClass = $(e.relatedTarget).data('show')
...@@ -113,6 +113,7 @@ class @Milestone ...@@ -113,6 +113,7 @@ class @Milestone
$(currentTabClass).removeClass('hidden') $(currentTabClass).removeClass('hidden')
$(currentTabClass).show() $(currentTabClass).show()
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list", connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true, dropOnEmpty: true,
......
...@@ -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;
} }
......
...@@ -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
......
...@@ -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
......
...@@ -264,11 +264,9 @@ class IssuableFinder ...@@ -264,11 +264,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 })
......
...@@ -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
...@@ -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',
......
# 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
......
...@@ -140,9 +140,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -140,9 +140,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) }
scope :references_project, -> { references(:target_project) } scope :references_project, -> { references(:target_project) }
......
...@@ -25,12 +25,13 @@ class Milestone < ActiveRecord::Base ...@@ -25,12 +25,13 @@ class Milestone < ActiveRecord::Base
include Referable include Referable
include StripAttribute include StripAttribute
include Elastic::MilestonesSearch include Elastic::MilestonesSearch
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) }
...@@ -93,30 +94,6 @@ class Milestone < ActiveRecord::Base ...@@ -93,30 +94,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?
......
...@@ -231,6 +231,7 @@ class Project < ActiveRecord::Base ...@@ -231,6 +231,7 @@ class Project < ActiveRecord::Base
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 :mirror, -> { where(mirror: true) } scope :mirror, -> { where(mirror: true) }
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
......
...@@ -144,18 +144,18 @@ class Repository ...@@ -144,18 +144,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)
...@@ -166,12 +166,12 @@ class Repository ...@@ -166,12 +166,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
...@@ -221,6 +221,14 @@ class Repository ...@@ -221,6 +221,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
...@@ -316,6 +324,16 @@ class Repository ...@@ -316,6 +324,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)
...@@ -351,9 +369,17 @@ class Repository ...@@ -351,9 +369,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.
...@@ -368,12 +394,21 @@ class Repository ...@@ -368,12 +394,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)
...@@ -953,6 +988,12 @@ class Repository ...@@ -953,6 +988,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
......
...@@ -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
......
...@@ -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, mirror_update: false) def execute(project, user, oldrev, newrev, ref, mirror_update: false)
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)
......
%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
%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}")
...@@ -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
%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,10 +42,11 @@ ...@@ -42,10 +42,11 @@
= 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.
<<<<<<< HEAD
.context.prepend-top-default .context.prepend-top-default
.milestone-summary .milestone-summary
%h4 Progress %h4 Progress
...@@ -153,3 +154,7 @@ ...@@ -153,3 +154,7 @@
%span.issues-count %span.issues-count
= link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do
= pluralize label.closed_issues_count, 'closed issue' = pluralize label.closed_issues_count, 'closed issue'
=======
= render 'shared/milestones/summary', milestone: @milestone, project: @project
= render 'shared/milestones/tabs', milestone: @milestone
>>>>>>> e8cd04e831a2db36c4029f2c193fc40d2568c79e
-# @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
...@@ -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
...@@ -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
......
...@@ -153,6 +153,8 @@ Parameters: ...@@ -153,6 +153,8 @@ Parameters:
"name": "John Smith", "name": "John Smith",
"state": "active", "state": "active",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null, "bio": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
......
# GitLab CI API # GitLab CI API
## Resources ## Purpose
- [Projects](projects.md)
- [Runners](runners.md)
- [Commits](commits.md)
- [Builds](builds.md)
## Authentication
GitLab CI API uses different types of authentication depends on what API you use.
Each API document has section with information about authentication you need to use.
GitLab CI API has 4 authentication methods:
* GitLab user token & GitLab url
* GitLab CI project token
* GitLab CI runners registration token
* GitLab CI runner token
### Authentication #1: GitLab user token & GitLab url
Authentication is done by
sending the `private-token` of a valid user and the `url` of an
authorized GitLab instance via a query string along with the API
request:
GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
If preferred, you may instead send the `private-token` as a header in Main purpose of GitLab CI API is to provide necessary data and context for
your request: GitLab CI Runners.
curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/" For consumer API take a look at this [documentation](../../api/README.md) where
you will find all relevant information.
## API Prefix
### Authentication #2: GitLab CI project token Current CI API prefix is `/ci/api/v1`.
Each project in GitLab CI has it own token. You need to prepend this prefix to all examples in this documentation, like:
It can be used to get project commits and builds information.
You can use project token only for certain project.
### Authentication #3: GitLab CI runners registration token GET /ci/api/v1/builds/:id/artifacts
This token is not persisted and is generated on each application start. ## Resources
It can be used only for registering new runners in system. You can find it on
GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners
### Authentication #4: GitLab CI runner token
Every GitLab CI runner has it own token that allow it to receive and update
GitLab CI builds. This token exists of internal purposes and should be used only
by runners
## JSON
All API requests are serialized using JSON. You don't need to specify
`.json` at the end of API URL.
## Status codes
The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave.
API request types:
- `GET` requests access one or more resources and return the result as JSON
- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not.
The following list shows the possible return codes for API requests.
Return values:
- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON - [Builds](builds.md)
- `201 Created` - The `POST` request was successful and the resource is returned as JSON - [Runners](runners.md)
- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
- `405 Method Not Allowed` - The request is not supported
- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
- `422 Unprocessable` - The entity could not be processed
- `500 Server Error` - While handling the request something went wrong on the server side
# Builds API # Builds API
This API used by runners to receive and update builds. API used by runners to receive and update builds.
__Authentication is done by runner token__ _**Note:** This API is intended to be used only by Runners as their own
communication channel. For the consumer API see the
[Builds API](../../api/builds.md)._
## Authentication
This API uses two types of authentication:
1. Unique runner's token
Token assigned to runner after it has been registered.
2. Using build authorization token
This is project's CI token that can be found in Continuous Integration
project settings.
Build authorization token can be passed as a parameter or a value of
`BUILD-TOKEN` header. This method are interchangeable.
## Builds ## Builds
### Runs oldest pending build by runner ### Runs oldest pending build by runner
POST /ci/builds/register POST /ci/api/v1/builds/register
Parameters: Parameters:
* `token` (required) - The unique token of runner * `token` (required) - Unique runner token
Returns:
```json
{
"id": 48584,
"ref": "0.1.1",
"tag": true,
"sha": "d63117656af6ff57d99e50cc270f854691f335ad",
"status": "success",
"name": "pages",
"token": "9dd60b4f1a439d1765357446c1084c",
"stage": "test",
"project_id": 479,
"project_name": "test",
"commands": "echo commands",
"repo_url": "http://gitlab-ci-token:token@gitlab.example/group/test.git",
"before_sha": "0000000000000000000000000000000000000000",
"allow_git_fetch": false,
"options": {
"image": "docker:image",
"artifacts": {
"paths": [
"public"
]
},
"cache": {
"paths": [
"vendor"
]
}
},
"timeout": 3600,
"variables": [
{
"key": "CI_BUILD_TAG",
"value": "0.1.1",
"public": true
}
],
"depends_on_builds": [
{
"id": 48584,
"ref": "0.1.1",
"tag": true,
"sha": "d63117656af6ff57d99e50cc270f854691f335ad",
"status": "success",
"name": "build",
"token": "9dd60b4f1a439d1765357446c1084c",
"stage": "build",
"project_id": 479,
"project_name": "test",
"artifacts_file": {
"filename": "artifacts.zip",
"size": 0
}
}
]
}
```
### Update details of an existing build ### Update details of an existing build
PUT /ci/builds/:id PUT /ci/api/v1/builds/:id
Parameters: Parameters:
* `id` (required) - The ID of a project * `id` (required) - The ID of a project
* `token` (required) - Unique runner token
* `state` (optional) - The state of a build * `state` (optional) - The state of a build
* `trace` (optional) - The trace of a build * `trace` (optional) - The trace of a build
### Upload artifacts to build
POST /ci/api/v1/builds/:id/artifacts
Parameters:
* `id` (required) - The ID of a build
* `token` (required) - The build authorization token
* `file` (required) - Artifacts file
### Download the artifacts file from build
GET /ci/api/v1/builds/:id/artifacts
Parameters:
* `id` (required) - The ID of a build
* `token` (required) - The build authorization token
### Remove the artifacts file from build
DELETE /ci/api/v1/builds/:id/artifacts
Parameters:
* ` id` (required) - The ID of a build
* `token` (required) - The build authorization token
# Commits API
**DEPRECATED**
Since GitLab 8.1, there is a new commit status API. Please see the [revised
documentation](../../api/commits.md#commit-status).
---
__Authentication is done by GitLab CI project token__
## Commits
### Retrieve all commits per project
Get list of commits per project
GET /ci/commits
Parameters:
* `project_id` (required) - The ID of a project
* `project_token` (requires) - Project token
* `page` (optional)
* `per_page` (optional) - items per request (default is 20)
Returns:
```json
[{
"id": 3,
"ref": "master",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"project_id": 2,
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"created_at": "2014-11-05T09:46:35.247Z",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"duration": 5.062692165374756,
"git_commit_message": "wow\n",
"git_author_name": "Administrator",
"git_author_email": "admin@example.com",
"builds": [{
"id": 7,
"project_id": 2,
"ref": "master",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"created_at": "2014-11-05T09:46:35.259Z",
"updated_at": "2014-11-05T09:46:44.255Z",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"started_at": "2014-11-05T09:46:39.192Z",
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"runner_id": 1,
"coverage": null,
"commit_id": 3
}]
}]
```
### Create commit
Inform GitLab CI about new commit you want it to build.
__If commit already exists in GitLab CI it will not be created__
POST /ci/commits
Parameters:
* `project_id` (required) - The ID of a project
* `project_token` (requires) - Project token
* `data` (required) - Push data. For example see comment in `lib/api/commits.rb`
Returns:
```json
{
"id": 3,
"ref": "master",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"project_id": 2,
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"created_at": "2014-11-05T09:46:35.247Z",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"duration": 5.062692165374756,
"git_commit_message": "wow\n",
"git_author_name": "Administrator",
"git_author_email": "admin@example.com",
"builds": [{
"id": 7,
"project_id": 2,
"ref": "master",
"status": "success",
"finished_at": "2014-11-05T09:46:44.254Z",
"created_at": "2014-11-05T09:46:35.259Z",
"updated_at": "2014-11-05T09:46:44.255Z",
"sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
"started_at": "2014-11-05T09:46:39.192Z",
"before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
"runner_id": 1,
"coverage": null,
"commit_id": 3
}]
}
```
# Projects API
This API is intended to aid in the setup and configuration of
projects on GitLab CI.
__Authentication is done by GitLab user token & GitLab url__
## Projects
### List Authorized Projects
Lists all projects that the authenticated user has access to.
```
GET /ci/projects
```
Returns:
```json
[
{
"id" : 271,
"name" : "gitlabhq",
"timeout" : 1800,
"token" : "iPWx6WM4lhHNedGfBpPJNP",
"default_ref" : "master",
"gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
"path" : "gitlab/gitlab-shell",
"always_build" : false,
"polling_interval" : null,
"public" : false,
"ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
"gitlab_id" : 3
},
{
"id" : 272,
"name" : "gitlab-ci",
"timeout" : 1800,
"token" : "iPWx6WM4lhHNedGfBpPJNP",
"default_ref" : "master",
"gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
"path" : "gitlab/gitlab-shell",
"always_build" : false,
"polling_interval" : null,
"public" : false,
"ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
"gitlab_id" : 4
}
]
```
### List Owned Projects
Lists all projects that the authenticated user owns.
```
GET /ci/projects/owned
```
Returns:
```json
[
{
"id" : 272,
"name" : "gitlab-ci",
"timeout" : 1800,
"token" : "iPWx6WM4lhHNedGfBpPJNP",
"default_ref" : "master",
"gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
"path" : "gitlab/gitlab-shell",
"always_build" : false,
"polling_interval" : null,
"public" : false,
"ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
"gitlab_id" : 4
}
]
```
### Single Project
Returns information about a single project for which the user is
authorized.
GET /ci/projects/:id
Parameters:
* `id` (required) - The ID of the GitLab CI project
### Create Project
Creates a GitLab CI project using GitLab project details.
POST /ci/projects
Parameters:
* `name` (required) - The name of the project
* `gitlab_id` (required) - The ID of the project on the GitLab instance
* `default_ref` (optional) - The branch to run on (default to `master`)
### Update Project
Updates a GitLab CI project using GitLab project details that the
authenticated user has access to.
PUT /ci/projects/:id
Parameters:
* `name` - The name of the project
* `default_ref` - The branch to run on (default to `master`)
### Remove Project
Removes a GitLab CI project that the authenticated user has access to.
DELETE /ci/projects/:id
Parameters:
* `id` (required) - The ID of the GitLab CI project
### Link Project to Runner
Links a runner to a project so that it can make builds (only via
authorized user).
POST /ci/projects/:id/runners/:runner_id
Parameters:
* `id` (required) - The ID of the GitLab CI project
* `runner_id` (required) - The ID of the GitLab CI runner
### Remove Project from Runner
Removes a runner from a project so that it can not make builds (only
via authorized user).
DELETE /ci/projects/:id/runners/:runner_id
Parameters:
* `id` (required) - The ID of the GitLab CI project
* `runner_id` (required) - The ID of the GitLab CI runner
\ No newline at end of file
# Runners API # Runners API
API used by runners to register and delete themselves.
_**Note:** This API is intended to be used only by Runners as their own _**Note:** This API is intended to be used only by Runners as their own
communication channel. For the consumer API see the communication channel. For the consumer API see the
[new Runners API](../../api/runners.md)._ [new Runners API](../../api/runners.md)._
## Runners ## Authentication
### Retrieve all runners
__Authentication is done by GitLab user token & GitLab url__ This API uses two types of authentication:
Used to get information about all runners registered on the GitLab CI 1. Unique runner's token
instance.
GET /ci/runners Token assigned to runner after it has been registered.
Returns: 2. Using runners' registration token
```json This is a token that can be found in project's settings.
[ It can be also found in Admin area &raquo; Runners settings.
{
"id" : 85,
"token" : "12b68e90394084703135"
},
{
"id" : 86,
"token" : "76bf894e969364709864"
},
]
```
### Register a new runner There are two types of tokens you can pass - shared runner registration
token or project specific registration token.
## Runners
__Authentication is done with a Shared runner registration token or a project Specific runner registration token__ ### Register a new runner
Used to make GitLab CI aware of available runners. Used to make GitLab CI aware of available runners.
POST /ci/runners/register POST /ci/api/v1/runners/register
Parameters: Parameters:
* `token` (required) - The registration token. It is 2 types of token you can pass here. * `token` (required) - Registration token
1. Shared runner registration token
2. Project specific registration token
Returns:
```json
{
"id" : 85,
"token" : "12b68e90394084703135"
}
```
### Delete a runner ### Delete a runner
Used to remove runner.
__Authentication is done by runner token__ DELETE /ci/api/v1/runners/delete
Used to removing runners.
DELETE /ci/runners/delete
Parameters: Parameters:
* `token` (required) - The runner token. * `token` (required) - Unique runner token
Returns:
```json
{
"id" : 1,
"token" : "d14963981a428f70121777e50643d1",
"created_at" : "2015-02-26T11:39:39.232Z",
"updated_at" : "2015-02-26T11:39:39.232Z",
"description" : "awesome runner"
}
```
...@@ -9,4 +9,5 @@ ...@@ -9,4 +9,5 @@
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md)
- [SQL guidelines](sql.md) for SQL guidelines
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
# SQL Query Guidelines
This document describes various guidelines to follow when writing SQL queries,
either using ActiveRecord/Arel or raw SQL queries.
## Using LIKE Statements
The most common way to search for data is using the `LIKE` statement. For
example, to get all issues with a title starting with "WIP:" you'd write the
following query:
```sql
SELECT *
FROM issues
WHERE title LIKE 'WIP:%';
```
On PostgreSQL the `LIKE` statement is case-sensitive. On MySQL this depends on
the case-sensitivity of the collation, which is usually case-insensitive. To
perform a case-insensitive `LIKE` on PostgreSQL you have to use `ILIKE` instead.
This statement in turn isn't supported on MySQL.
To work around this problem you should write `LIKE` queries using Arel instead
of raw SQL fragments as Arel automatically uses `ILIKE` on PostgreSQL and `LIKE`
on MySQL. This means that instead of this:
```ruby
Issue.where('title LIKE ?', 'WIP:%')
```
You'd write this instead:
```ruby
Issue.where(Issue.arel_table[:title].matches('WIP:%'))
```
Here `matches` generates the correct `LIKE` / `ILIKE` statement depending on the
database being used.
If you need to chain multiple `OR` conditions you can also do this using Arel:
```ruby
table = Issue.arel_table
Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%')))
```
For PostgreSQL this produces:
```sql
SELECT *
FROM issues
WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%')
```
In turn for MySQL this produces:
```sql
SELECT *
FROM issues
WHERE (title LIKE 'WIP:%' OR foo LIKE 'WIP:%')
```
## LIKE & Indexes
Neither PostgreSQL nor MySQL use any indexes when using `LIKE` / `ILIKE` with a
wildcard at the start. For example, this will not use any indexes:
```sql
SELECT *
FROM issues
WHERE title ILIKE '%WIP:%';
```
Because the value for `ILIKE` starts with a wildcard the database is not able to
use an index as it doesn't know where to start scanning the indexes.
MySQL provides no known solution to this problem. Luckily PostgreSQL _does_
provide a solution: trigram GIN indexes. These indexes can be created as
follows:
```sql
CREATE INDEX [CONCURRENTLY] index_name_here
ON table_name
USING GIN(column_name gin_trgm_ops);
```
The key here is the `GIN(column_name gin_trgm_ops)` part. This creates a [GIN
index][gin-index] with the operator class set to `gin_trgm_ops`. These indexes
_can_ be used by `ILIKE` / `LIKE` and can lead to greatly improved performance.
One downside of these indexes is that they can easily get quite large (depending
on the amount of data indexed).
To keep naming of these indexes consistent please use the following naming
pattern:
index_TABLE_on_COLUMN_trigram
For example, a GIN/trigram index for `issues.title` would be called
`index_issues_on_title_trigram`.
Due to these indexes taking quite some time to be built they should be built
concurrently. This can be done by using `CREATE INDEX CONCURRENTLY` instead of
just `CREATE INDEX`. Concurrent indexes can _not_ be created inside a
transaction. Transactions for migrations can be disabled using the following
pattern:
```ruby
class MigrationName < ActiveRecord::Migration
disable_ddl_transaction!
end
```
For example:
```ruby
class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));'
execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));'
end
def down
return unless Gitlab::Database.postgresql?
remove_index :users, :index_on_users_lower_username
remove_index :users, :index_on_users_lower_email
end
end
```
## Plucking IDs
This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a
set of values into memory only to use them as an argument for another query. For
example, this will make the database **very** sad:
```ruby
projects = Project.all.pluck(:id)
MergeRequest.where(source_project_id: projects)
```
Instead you can just use sub-queries which perform far better:
```ruby
MergeRequest.where(source_project_id: Project.all.select(:id))
```
The _only_ time you should use `pluck` is when you actually need to operate on
the values in Ruby itself (e.g. write them to a file). In almost all other cases
you should ask yourself "Can I not just use a sub-query?".
## Use UNIONs
UNIONs aren't very commonly used in most Rails applications but they're very
powerful and useful. In most applications queries tend to use a lot of JOINs to
get related data or data based on certain criteria, but JOIN performance can
quickly deteriorate as the data involved grows.
For example, if you want to get a list of projects where the name contains a
value _or_ the name of the namespace contains a value most people would write
the following query:
```sql
SELECT *
FROM projects
JOIN namespaces ON namespaces.id = projects.namespace_id
WHERE projects.name ILIKE '%gitlab%'
OR namespaces.name ILIKE '%gitlab%';
```
Using a large database this query can easily take around 800 milliseconds to
run. Using a UNION we'd write the following instead:
```sql
SELECT projects.*
FROM projects
WHERE projects.name ILIKE '%gitlab%'
UNION
SELECT projects.*
FROM projects
JOIN namespaces ON namespaces.id = projects.namespace_id
WHERE namespaces.name ILIKE '%gitlab%';
```
This query in turn only takes around 15 milliseconds to complete while returning
the exact same records.
This doesn't mean you should start using UNIONs everywhere, but it's something
to keep in mind when using lots of JOINs in a query and filtering out records
based on the joined data.
GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a UNION
of multiple `ActiveRecord::Relation` objects. You can use this class as
follows:
```ruby
union = Gitlab::SQL::Union.new([projects, more_projects, ...])
Project.from("(#{union.to_sql}) projects")
```
## Ordering by Creation Date
When ordering records based on the time they were created you can simply order
by the `id` column instead of ordering by `created_at`. Because IDs are always
unique and incremented in the order that rows are created this will produce the
exact same results. This also means there's no need to add an index on
`created_at` to ensure consistent performance as `id` is already indexed by
default.
[gin-index]: http://www.postgresql.org/docs/current/static/gin.html
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab **Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks. server. Only GitLab server administrators will be able to complete these tasks.
Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Please explore [web hooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions. Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update. Examples of server-side git hooks include pre-receive, post-receive, and update.
......
...@@ -474,12 +474,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj ...@@ -474,12 +474,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj
### Initial Login ### Initial Login
Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in: Visit YOUR_SERVER in your web browser for your first GitLab login.
root If you didn't [provide a root password during setup](#initialize-database-and-activate-advanced-features),
5iveL!fe you'll be redirected to a password reset screen to provide the password for the
initial administrator account. Enter your desired password and you'll be
redirected back to the login screen.
**Important Note:** On login you'll be prompted to change the password. The default account's username is **root**. Provide the password you created
earlier and login. After login you can change the username if you wish.
**Enjoy!** **Enjoy!**
......
...@@ -41,3 +41,34 @@ please see the [project_services directory][projects-code]. ...@@ -41,3 +41,34 @@ please see the [project_services directory][projects-code].
[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html [jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
[Project Service]: ../project_services/project_services.md [Project Service]: ../project_services/project_services.md
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services [projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
## SSL certificate errors
When trying to integrate GitLab with services that are using self-signed certificates,
it is very likely that SSL certificate errors will occur on different parts of the
application, most likely Sidekiq. There are 2 approaches you can take to solve this:
1. Add the root certificate to the trusted chain of the OS.
1. If using Omnibus, you can add the certificate to GitLab's trusted certificates.
**OS main trusted chain**
This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html)
has all the information you need to add a certificate to the main trusted chain.
This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu)
at SuperUser also has relevant information.
**Omnibus Trusted Chain**
It is enough to concatenate the certificate to the main trusted certificate:
```bash
cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
```
After that restart GitLab with:
```bash
sudo gitlab-ctl restart
```
...@@ -479,3 +479,25 @@ When setting `method: ssl`, the underlying authentication method used by ...@@ -479,3 +479,25 @@ When setting `method: ssl`, the underlying authentication method used by
`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with `omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
the LDAP server before any LDAP-protocol data is exchanged but no validation of the LDAP server before any LDAP-protocol data is exchanged but no validation of
the LDAP server's SSL certificate is performed. the LDAP server's SSL certificate is performed.
## Troubleshooting
### Invalid credentials when logging in
Make sure the user you are binding with has enough permissions to read the user's
tree and traverse it.
Also make sure that the `user_filter` is not blocking otherwise valid users.
To make sure that the LDAP settings are correct and GitLab can see your users,
execute the following command:
```bash
# For Omnibus installations
sudo gitlab-rake gitlab:ldap:check
# For installations from source
sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
```
...@@ -133,12 +133,16 @@ will be returned to GitLab and will be signed in. ...@@ -133,12 +133,16 @@ will be returned to GitLab and will be signed in.
## Troubleshooting ## Troubleshooting
### 500 error after login
If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page,
this likely indicates that GitLab could not get the email address for the SAML user. this likely indicates that GitLab could not get the email address for the SAML user.
Make sure the IdP provides a claim containing the user's email address, using claim name Make sure the IdP provides a claim containing the user's email address, using claim name
`email` or `mail`. `email` or `mail`.
### Redirect back to login screen with no evident error
If after signing in into your SAML server you are redirected back to the sign in page and If after signing in into your SAML server you are redirected back to the sign in page and
no error is displayed, check your `production.log` file. It will most likely contain the no error is displayed, check your `production.log` file. It will most likely contain the
message `Can't verify CSRF token authenticity`. This means that there is an error during message `Can't verify CSRF token authenticity`. This means that there is an error during
...@@ -148,3 +152,35 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th ...@@ -148,3 +152,35 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th
`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, `omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab,
where it can then be seen in the usual logs, or as a flash message in the login where it can then be seen in the usual logs, or as a flash message in the login
screen. screen.
### Invalid audience
This error means that the IdP doesn't recognize GitLab as a valid sender and
receiver of SAML requests. Make sure to add the GitLab callback URL to the approved
audiences of the IdP server.
### Missing claims
The IdP server needs to pass certain information in order for GitLab to either
create an account, or match the login information to an existing account. `email`
is the minimum amount of information that needs to be passed. If the IdP server
is not providing this information, all SAML requests will fail.
Make sure this information is provided.
### Key validation error, Digest mismatch or Fingerprint mismatch
These errors all come from a similar place, the SAML certificate. SAML requests
need to be validated using a fingerprint, a certificate or a validator.
For this you need take the following into account:
- If no certificate is provided in the settings, a fingerprint or fingerprint
validator needs to be provided and the response from the server must contain
a certificate (`<ds:KeyInfo><ds:X509Data><ds:X509Certificate>`)
- If a certificate is provided in the settings, it is no longer necessary for
the request to contain one. In this case the fingerprint or fingerprint
validators are optional
Make sure that one of the above described scenarios is valid, or the requests will
fail with one of the mentioned errors.
\ No newline at end of file
...@@ -2,19 +2,11 @@ ...@@ -2,19 +2,11 @@
## On Slack ## On Slack
To enable Slack integration you must create an Incoming WebHooks integration on Slack; To enable Slack integration you must create an Incoming WebHooks integration on Slack:
1. [Sign in to Slack](https://slack.com/signin) 1. [Sign in to Slack](https://slack.com/signin)
1. Select **Apps & Custom Integrations** from the dropdown next to your team name. 1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
1. Click the **Configure** link (right-upper corner).
1. Select the **Custom integrations** tab.
1. Click the **Incoming WebHooks** row.
1. Click the **Add configuration** button.
1. Choose the channel name you want to send notifications to. 1. Choose the channel name you want to send notifications to.
......
...@@ -219,3 +219,16 @@ You can see from the above image that there are four references to GitLab: ...@@ -219,3 +219,16 @@ You can see from the above image that there are four references to GitLab:
[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" [JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website"
[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" [jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service"
[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" [8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post"
## Troubleshooting
### GitLab is unable to comment on a ticket
Make sure that the user you set up for GitLab to communicate with JIRA has the
correct access permission to post comments on a ticket and to also transition the
ticket, if you'd like GitLab to also take care of closing them.
### GitLab is unable to close a ticket
Make sure the the `Transition ID` you set within the JIRA settings matches the
one your project needs to close a ticket.
...@@ -64,6 +64,9 @@ sudo -u git -H bundle install --without postgres development test --deployment ...@@ -64,6 +64,9 @@ sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql') # PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations # Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
......
...@@ -62,7 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment ...@@ -62,7 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment
# MySQL # MySQL
sudo -u git -H bundle install --without development test postgres --deployment sudo -u git -H bundle install --without development test postgres --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
``` ```
......
...@@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with ...@@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with
## Configuration ## Configuration
Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on. Git LFS objects can be large in size. By default, they are stored on the server
GitLab is installed on.
There are two configuration options to help GitLab server administrators: There are two configuration options to help GitLab server administrators:
...@@ -37,5 +38,8 @@ In `config/gitlab.yml`: ...@@ -37,5 +38,8 @@ In `config/gitlab.yml`:
## Known limitations ## Known limitations
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
is not supported
* Currently, removing LFS objects from GitLab Git LFS storage is not supported * Currently, removing LFS objects from GitLab Git LFS storage is not supported
* LFS authentications via SSH is not supported for the time being
* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
# Git LFS # Git LFS
Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. Managing large files such as audio, video and graphics files has always been one
The general recommendation is to not have Git repositories larger than 1GB to preserve performance. of the shortcomings of Git. The general recommendation is to not have Git repositories
larger than 1GB to preserve performance.
GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
environments it is not always convenient to use different commands to differentiate between the large files and regular ones. (EE only), however in certain environments it is not always convenient to use
different commands to differentiate between the large files and regular ones.
Git LFS makes this simpler for the end user by removing the requirement to learn new commands. Git LFS makes this simpler for the end user by removing the requirement to
learn new commands.
## How it works ## How it works
Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests. Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file. to authorize client requests. Once the request is authorized, Git LFS client receives
instructions from where to fetch or where to push the large file.
## GitLab server configuration ## GitLab server configuration
...@@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do ...@@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do
## Known limitations ## Known limitations
* Git LFS v1 original API is not supported since it was deprecated early in LFS development * Git LFS v1 original API is not supported since it was deprecated early in LFS
development
* When SSH is set as a remote, Git LFS objects still go through HTTPS * When SSH is set as a remote, Git LFS objects still go through HTTPS
* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended * Any Git LFS request will ask for HTTPS credentials to be provided so good Git
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting) credentials store is recommended
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
to add the URL to Git config manually (see #troubleshooting)
## Using Git LFS ## Using Git LFS
Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS: Lets take a look at the workflow when you need to check large files into your Git
For example, if you want to upload a very large file and check it into your Git repository: repository with Git LFS. For example, if you want to upload a very large file and
check it into your Git repository:
```bash ```bash
git clone git@gitlab.example.com:group/project.git git clone git@gitlab.example.com:group/project.git
...@@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project ...@@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project
git lfs track "*.iso" # select the file extensions that you want to treat as large files git lfs track "*.iso" # select the file extensions that you want to treat as large files
``` ```
Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension: Once a certain file extension is marked for tracking as a LFS object you can use
Git as usual without having to redo the command to track a file with the same extension:
```bash ```bash
cp ~/tmp/debian.iso ./ # copy a large file into the current directory cp ~/tmp/debian.iso ./ # copy a large file into the current directory
...@@ -49,13 +58,17 @@ git commit -am "Added Debian iso" # commit the file meta data ...@@ -49,13 +58,17 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server git push origin master # sync the git repo and large file to the GitLab server
``` ```
Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication. Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP
authentication.
```bash ```bash
git clone git@gitlab.example.com:group/project.git git clone git@gitlab.example.com:group/project.git
``` ```
If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`: If you already cloned the repository and you want to get the latest LFS object
that are on the remote repository, eg. from branch `master`:
```bash ```bash
git lfs fetch master git lfs fetch master
...@@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project. ...@@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project.
* Project is not allowed to access the LFS object * Project is not allowed to access the LFS object
LFS object you are trying to push to the project or fetch from the project is not available to the project anymore. LFS object you are trying to push to the project or fetch from the project is not
Probably the object was removed from the server. available to the project anymore. Probably the object was removed from the server.
* Local git repository is using deprecated LFS API * Local git repository is using deprecated LFS API
...@@ -89,16 +102,26 @@ git lfs logs last ...@@ -89,16 +102,26 @@ git lfs logs last
If the status `error 501` is shown, it is because: If the status `error 501` is shown, it is because:
* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support. * Git LFS support is not enabled on the GitLab server. Check with your GitLab
administrator why Git LFS is not enabled on the server. See
[LFS administration documentation](lfs_administration.md) for instructions
on how to enable LFS support.
* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported. * Git LFS client version is not supported by GitLab server. Check your Git LFS
version with `git lfs version`. Check the Git config of the project for traces
of deprecated API with `git lfs -l`. If `batch = false` is set in the config,
remove the line and try to update your Git LFS client. Only version 1.0.1 and
newer are supported.
### getsockopt: connection refused ### getsockopt: connection refused
If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, If you push a LFS object to a project and you receive an error similar to:
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP. `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab
instance is being served on HTTP.
This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config. This behaviour is caused by Git LFS using HTTPS connections by default when a
`lfsurl` is not set in the Git config.
To prevent this from happening, set the lfs url in project Git config: To prevent this from happening, set the lfs url in project Git config:
...@@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o ...@@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o
### Credentials are always required when pushing an object ### Credentials are always required when pushing an object
Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required. Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
the LFS object on every push for every object, user HTTPS credentials are required.
By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). By default, Git has support for remembering the credentials for each repository
you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
For example, you can tell Git to remember the password for a period of time in which you expect to push the objects: For example, you can tell Git to remember the password for a period of time in
which you expect to push the objects:
```bash ```bash
git config --global credential.helper 'cache --timeout=3600' git config --global credential.helper 'cache --timeout=3600'
``` ```
This will remember the credentials for an hour after which Git operations will require re-authentication. This will remember the credentials for an hour after which Git operations will
require re-authentication.
If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). If you are using OS X you can use `osxkeychain` to store and encrypt your credentials.
For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). More details about various methods of storing the user credentials can be found
\ No newline at end of file on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
\ No newline at end of file
...@@ -28,3 +28,20 @@ Feature: Group Milestones ...@@ -28,3 +28,20 @@ Feature: Group Milestones
And I fill milestone name And I fill milestone name
When I press create mileston button When I press create mileston button
Then milestone in each project should be created Then milestone in each project should be created
Scenario: I should see Issues listed with labels
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
Then I should see the "bug" label
And I should see the "feature" label
And I should see the project name in the Issue row
Scenario: I should see the Labels tab
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
And I click on the "Labels" tab
Then I should see the list of labels
...@@ -58,14 +58,6 @@ Feature: Project Issues ...@@ -58,14 +58,6 @@ Feature: Project Issues
Then I should see comment "XML attached" Then I should see comment "XML attached"
And I should see an error alert section within the comment form And I should see an error alert section within the comment form
@javascript
Scenario: Visiting Issues after leaving a comment
Given I visit issue page "Release 0.4"
And I leave a comment like "XML attached"
And I visit project "Shop" issues page
And I sort the list by "Last updated"
Then I should see "Release 0.4" at the top
@javascript @javascript
Scenario: Visiting Issues after being sorted the list Scenario: Visiting Issues after being sorted the list
Given I visit project "Shop" issues page Given I visit project "Shop" issues page
......
...@@ -86,15 +86,6 @@ Feature: Project Merge Requests ...@@ -86,15 +86,6 @@ Feature: Project Merge Requests
And I leave a comment like "XML attached" And I leave a comment like "XML attached"
Then I should see comment "XML attached" Then I should see comment "XML attached"
@javascript
Scenario: Visiting Merge Requests after leaving a comment
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-04"
And I leave a comment like "XML attached"
And I visit project "Shop" merge requests page
And I sort the list by "Last updated"
Then I should see "Bug NS-04" at the top
@javascript @javascript
Scenario: Visiting Merge Requests after being sorted the list Scenario: Visiting Merge Requests after being sorted the list
Given I visit project "Shop" merge requests page Given I visit project "Shop" merge requests page
...@@ -128,16 +119,6 @@ Feature: Project Merge Requests ...@@ -128,16 +119,6 @@ Feature: Project Merge Requests
And I sort the list by "Least popular" And I sort the list by "Least popular"
Then The list should be sorted by "Least popular" Then The list should be sorted by "Least popular"
@javascript
Scenario: Visiting Merge Requests after commenting on diffs
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on diff
And I visit project "Shop" merge requests page
And I sort the list by "Last updated"
Then I should see "Bug NS-05" at the top
@javascript @javascript
Scenario: I comment on a merge request diff Scenario: I comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside Given project "Shop" have "Bug NS-05" open merge request with diffs inside
......
...@@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end end
step 'I click on one group milestone' do step 'I click on one group milestone' do
milestones = Milestone.where(title: 'GL-113')
@global_milestone = GlobalMilestone.new('GL-113', milestones)
click_link 'GL-113' click_link 'GL-113'
end end
...@@ -33,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -33,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
step 'I should see group milestone with all issues and MRs assigned to that milestone' do step 'I should see group milestone with all issues and MRs assigned to that milestone' do
expect(page).to have_content('Milestone GL-113') expect(page).to have_content('Milestone GL-113')
expect(page).to have_content('Progress: 0 closed – 3 open') expect(page).to have_content('3 issues: 3 open and 0 closed')
issue = Milestone.find_by(name: 'GL-113').issues.first issue = Milestone.find_by(name: 'GL-113').issues.first
expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue)) expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
end end
...@@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end end
end end
step 'I should see the "bug" label' do
page.within('#tab-issues') do
expect(page).to have_content 'bug'
end
end
step 'I should see the "feature" label' do
page.within('#tab-issues') do
expect(page).to have_content 'bug'
end
end
step 'I should see the project name in the Issue row' do
page.within('#tab-issues') do
@global_milestone.projects.each do |project|
expect(page).to have_content project.name
end
end
end
step 'I click on the "Labels" tab' do
page.within('.nav-links') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
end
step 'I should see the list of labels' do
page.within('#tab-labels') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
end
private private
def group_milestone def group_milestone
...@@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
project = create :project, path: path, group: group project = create :project, path: path, group: group
milestone = create :milestone, title: "Version 7.2", project: project milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
create(:label, project: project, title: 'feature')
create :issue, create :issue,
project: project, project: project,
assignee: current_user, assignee: current_user,
...@@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
due_date: '2114-08-20', due_date: '2114-08-20',
description: 'Lorem Ipsum is simply dummy text' description: 'Lorem Ipsum is simply dummy text'
create :issue, issue = create :issue,
project: project, project: project,
assignee: current_user, assignee: current_user,
author: current_user, author: current_user,
milestone: milestone milestone: milestone
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end end
end end
end end
...@@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
end end
step 'I should see "Release 0.4" at the top' do
expect(page.find('ul.content-list.issues-list li.issue:first-child')).to have_content("Release 0.4")
end
def filter_issue(text) def filter_issue(text)
fill_in 'issue_search', with: text fill_in 'issue_search', with: text
end end
......
...@@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps ...@@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end end
step 'I should see 3 issues' do step 'I should see 3 issues' do
expect(page).to have_selector('#tab-issues li.issue-row', count: 4) expect(page).to have_selector('#tab-issues li.issuable-row', count: 4)
end end
step 'I click link to remove milestone' do step 'I click link to remove milestone' do
......
...@@ -642,14 +642,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -642,14 +642,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end end
end end
step 'I should see "Bug NS-05" at the top' do
expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05")
end
step 'I should see "Bug NS-04" at the top' do
expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04")
end
def merge_request def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end end
......
...@@ -144,11 +144,4 @@ module SharedNote ...@@ -144,11 +144,4 @@ module SharedNote
expect(page).to have_content("+1 Awesome!") expect(page).to have_content("+1 Awesome!")
end end
end end
step 'I sort the list by "Last updated"' do
find('button.dropdown-toggle.btn').click
page.within('ul.dropdown-menu.dropdown-menu-align-right li') do
click_link "Last updated"
end
end
end end
...@@ -23,6 +23,8 @@ module API ...@@ -23,6 +23,8 @@ module API
end end
class UserFull < User class UserFull < User
expose :last_sign_in_at
expose :confirmed_at
expose :email expose :email
expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity expose :identities, using: Entities::Identity
......
...@@ -60,7 +60,7 @@ describe Milestone, models: true do ...@@ -60,7 +60,7 @@ describe Milestone, models: true do
end end
it "should recover from dividing by zero" do it "should recover from dividing by zero" do
expect(milestone.issues).to receive(:count).and_return(0) expect(milestone.issues).to receive(:size).and_return(0)
expect(milestone.percent_complete).to eq(0) expect(milestone.percent_complete).to eq(0)
end end
end end
...@@ -114,7 +114,6 @@ describe Milestone, models: true do ...@@ -114,7 +114,6 @@ describe Milestone, models: true do
end end
it { expect(milestone.closed_items_count).to eq(1) } it { expect(milestone.closed_items_count).to eq(1) }
it { expect(milestone.open_items_count).to eq(2) }
it { expect(milestone.total_items_count).to eq(3) } it { expect(milestone.total_items_count).to eq(3) }
it { expect(milestone.is_empty?).to be_falsey } it { expect(milestone.is_empty?).to be_falsey }
end end
......
...@@ -24,7 +24,7 @@ require 'spec_helper' ...@@ -24,7 +24,7 @@ require 'spec_helper'
describe Note, models: true do describe Note, models: true do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:noteable) } it { is_expected.to belong_to(:noteable).touch(true) }
it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
......
...@@ -148,6 +148,12 @@ describe Repository, models: true do ...@@ -148,6 +148,12 @@ describe Repository, models: true do
expect(branch.name).to eq('new_feature') expect(branch.name).to eq('new_feature')
end end
it 'calls the after_create_branch hook' do
expect(repository).to receive(:after_create_branch)
repository.add_branch(user, 'new_feature', 'master')
end
end end
context 'when pre hooks failed' do context 'when pre hooks failed' do
...@@ -405,7 +411,7 @@ describe Repository, models: true do ...@@ -405,7 +411,7 @@ describe Repository, models: true do
end end
end end
describe '#expire_branch_ache' do describe '#expire_branch_cache' do
# This method is private but we need it for testing purposes. Sadly there's # This method is private but we need it for testing purposes. Sadly there's
# no other proper way of testing caching operations. # no other proper way of testing caching operations.
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
...@@ -556,11 +562,12 @@ describe Repository, models: true do ...@@ -556,11 +562,12 @@ describe Repository, models: true do
end end
end end
describe '#before_create_tag' do describe '#before_push_tag' do
it 'flushes the cache' do it 'flushes the cache' do
expect(repository).to receive(:expire_cache) expect(repository).to receive(:expire_cache)
expect(repository).to receive(:expire_tag_count_cache)
repository.before_create_tag repository.before_push_tag
end end
end end
...@@ -641,4 +648,89 @@ describe Repository, models: true do ...@@ -641,4 +648,89 @@ describe Repository, models: true do
end end
end end
end end
describe "#main_language" do
it 'shows the main language of the project' do
expect(repository.main_language).to eq("Ruby")
end
it 'returns nil when the repository is empty' do
allow(repository).to receive(:empty?).and_return(true)
expect(repository.main_language).to be_nil
end
end
describe '#before_remove_tag' do
it 'flushes the tag cache' do
expect(repository).to receive(:expire_tag_count_cache)
repository.before_remove_tag
end
end
describe '#branch_count' do
it 'returns the number of branches' do
expect(repository.branch_count).to be_an_instance_of(Fixnum)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
expect(repository.tag_count).to be_an_instance_of(Fixnum)
end
end
describe '#expire_branch_count_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do
expect(cache).to receive(:expire).with(:branch_count)
repository.expire_branch_count_cache
end
end
describe '#expire_tag_count_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do
expect(cache).to receive(:expire).with(:tag_count)
repository.expire_tag_count_cache
end
end
describe '#add_tag' do
it 'adds a tag' do
expect(repository).to receive(:before_push_tag)
expect_any_instance_of(Gitlab::Shell).to receive(:add_tag).
with(repository.path_with_namespace, '8.5', 'master', 'foo')
repository.add_tag('8.5', 'master', 'foo')
end
end
describe '#rm_branch' do
let(:user) { create(:user) }
it 'removes a branch' do
expect(repository).to receive(:before_remove_branch)
expect(repository).to receive(:after_remove_branch)
repository.rm_branch(user, 'feature')
end
end
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
with(repository.path_with_namespace, '8.5')
repository.rm_tag('8.5')
end
end
end end
...@@ -47,6 +47,8 @@ describe API::API, api: true do ...@@ -47,6 +47,8 @@ describe API::API, api: true do
expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'identities'
expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'can_create_project'
expect(json_response.first.keys).to include 'two_factor_enabled' expect(json_response.first.keys).to include 'two_factor_enabled'
expect(json_response.first.keys).to include 'last_sign_in_at'
expect(json_response.first.keys).to include 'confirmed_at'
end end
end end
......
require 'spec_helper'
describe DeleteTagService, services: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
let(:tag) { double(:tag, name: '8.5', target: 'abc123') }
describe '#execute' do
before do
allow(repository).to receive(:find_tag).and_return(tag)
end
it 'removes the tag' do
expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
and_return(true)
expect(repository).to receive(:before_remove_tag)
expect(service).to receive(:success)
service.execute('8.5')
end
end
end
...@@ -171,6 +171,23 @@ describe GitPushService, services: true do ...@@ -171,6 +171,23 @@ describe GitPushService, services: true do
end end
end end
describe "Updates main language" do
context "before push" do
it { expect(project.main_language).to eq(nil) }
end
context "after push" do
before do
@service = execute_service(project, user, @oldrev, @newrev, @ref)
end
it { expect(@service.update_main_language).to eq(true) }
it { expect(project.main_language).to eq("Ruby") }
end
end
describe "Web Hooks" do describe "Web Hooks" do
context "execute web hooks" do context "execute web hooks" do
it "when pushing a branch for the first time" do it "when pushing a branch for the first time" do
......
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