summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--CHANGELOG6
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock7
-rw-r--r--app/assets/javascripts/application.js.coffee28
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee2
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee6
-rw-r--r--app/assets/javascripts/sidebar.js.coffee18
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss20
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss10
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/controllers/projects/group_links_controller.rb23
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/finders/projects_finder.rb3
-rw-r--r--app/models/ci/build.rb34
-rw-r--r--app/models/ci/commit.rb34
-rw-r--r--app/models/commit_status.rb18
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/note.rb33
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_group_link.rb36
-rw-r--r--app/models/project_team.rb52
-rw-r--r--app/models/user.rb3
-rw-r--r--app/services/ci/image_for_build_service.rb2
-rw-r--r--app/services/create_commit_builds_service.rb1
-rw-r--r--app/views/admin/builds/_build.html.haml21
-rw-r--r--app/views/admin/groups/show.html.haml16
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/groups/_projects.html.haml13
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/groups/edit.html.haml9
-rw-r--r--app/views/groups/show.html.haml28
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml6
-rw-r--r--app/views/projects/builds/index.html.haml3
-rw-r--r--app/views/projects/builds/show.html.haml19
-rw-r--r--app/views/projects/ci/builds/_build.html.haml77
-rw-r--r--app/views/projects/commit/_builds.html.haml7
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml79
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml59
-rw-r--r--app/views/projects/group_links/index.html.haml41
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml21
-rw-r--r--app/views/projects/project_members/index.html.haml3
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb10
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb5
-rw-r--r--db/migrate/20150930110012_add_group_share_lock.rb5
-rw-r--r--db/schema.rb21
-rw-r--r--doc/api/builds.md7
-rw-r--r--doc/api/notes.md1
-rw-r--r--doc/api/project_snippets.md1
-rw-r--r--doc/api/projects.md14
-rw-r--r--doc/ci/yaml/README.md298
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/update/8.5-to-8.6.md164
-rw-r--r--doc/web_hooks/web_hooks.md1
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/groups/max_access_level.pngbin0 -> 135354 bytes
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin0 -> 118382 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin0 -> 118868 bytes
-rw-r--r--doc/workflow/share_projects_with_other_groups.md30
-rw-r--r--doc/workflow/share_with_group.md13
-rw-r--r--doc/workflow/share_with_group.pngbin0 -> 53784 bytes
-rw-r--r--features/admin/groups.feature5
-rw-r--r--features/project/group_links.feature16
-rw-r--r--features/project/network_graph.feature3
-rw-r--r--features/project/team_management.feature5
-rw-r--r--features/steps/admin/groups.rb19
-rw-r--r--features/steps/dashboard/issues.rb8
-rw-r--r--features/steps/dashboard/merge_requests.rb20
-rw-r--r--features/steps/project/issues/award_emoji.rb5
-rw-r--r--features/steps/project/network_graph.rb9
-rw-r--r--features/steps/project/project_group_links.rb50
-rw-r--r--features/steps/project/team_management.rb19
-rw-r--r--features/steps/shared/builds.rb2
-rw-r--r--lib/api/entities.rb14
-rw-r--r--lib/api/projects.rb27
-rw-r--r--lib/banzai/filter/sanitization_filter.rb9
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb27
-rw-r--r--spec/factories/project_group_links.rb6
-rw-r--r--spec/finders/projects_finder_spec.rb34
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb20
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb84
-rw-r--r--spec/models/build_spec.rb66
-rw-r--r--spec/models/ci/commit_spec.rb44
-rw-r--r--spec/models/note_spec.rb62
-rw-r--r--spec/models/project_group_link_spec.rb17
-rw-r--r--spec/models/project_team_spec.rb44
-rw-r--r--spec/requests/api/project_snippets_spec.rb18
-rw-r--r--spec/requests/api/projects_spec.rb36
94 files changed, 1624 insertions, 461 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bd013d50faa..1ba1d8d8468 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -150,11 +150,10 @@ bundler:audit:
stage: test
script:
- "bundle exec bundle-audit update"
- - "bundle exec bundle-audit check"
+ - "bundle exec bundle-audit check --ignore OSVDB-115941"
tags:
- ruby
- mysql
- allow_failure: true
# Ruby 2.2 jobs
diff --git a/CHANGELOG b/CHANGELOG
index b120810ebd8..d38646ece67 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,9 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
+ - Bump gitlab_git to 9.0.3 (Stan Hu)
- Support Golang subpackage fetching (Stan Hu)
+ - Bump Capybara gem to 2.6.2 (Stan Hu)
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
@@ -18,10 +20,14 @@ v 8.6.0 (unreleased)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang)
+ - Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
+ - Ignore jobs that start with `.` (hidden jobs)
+ - Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
+ - Allow to define on which builds the current one depends on
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip)
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index a04abec9149..bc02b8685c1 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.10
+2.6.11
diff --git a/Gemfile b/Gemfile
index 1550afb1b56..26b61c2fa5b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -273,7 +273,7 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.0.0'
- gem 'capybara', '~> 2.4.0'
+ gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index d4e28db00d6..9772e7fdd38 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -108,7 +108,8 @@ GEM
thor (~> 0.18)
byebug (8.2.1)
cal-heatmap-rails (3.5.1)
- capybara (2.4.4)
+ capybara (2.6.2)
+ addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
@@ -358,7 +359,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (9.0.1)
+ gitlab_git (9.0.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -901,7 +902,7 @@ DEPENDENCIES
bundler-audit
byebug
cal-heatmap-rails (~> 3.5.0)
- capybara (~> 2.4.0)
+ capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 1212e89975b..e9c6196e926 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -108,6 +108,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
@@ -256,35 +258,14 @@ $ ->
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
- bootstrapBreakpoint = undefined;
- checkBootstrapBreakpoints = ->
- if $('.device-xs').is(':visible')
- bootstrapBreakpoint = "xs"
- else if $('.device-sm').is(':visible')
- bootstrapBreakpoint = "sm"
- else if $('.device-md').is(':visible')
- bootstrapBreakpoint = "md"
- else if $('.device-lg').is(':visible')
- bootstrapBreakpoint = "lg"
-
- setBootstrapBreakpoints = ->
- if $('.device-xs').length
- return
-
- $("body")
- .append('<div class="device-xs visible-xs"></div>'+
- '<div class="device-sm visible-sm"></div>'+
- '<div class="device-md visible-md"></div>'+
- '<div class="device-lg visible-lg"></div>')
- checkBootstrapBreakpoints()
-
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
- checkBootstrapBreakpoints()
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
@@ -293,6 +274,5 @@ $ ->
.on "resize", (e) ->
fitSidebarForSize()
- setBootstrapBreakpoints()
checkInitialSidebarSize()
new Aside()
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..5457430f921
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,37 @@
+class @Breakpoints
+ instance = null;
+
+ class BreakpointInstance
+ BREAKPOINTS = ["xs", "sm", "md", "lg"]
+
+ constructor: ->
+ @setup()
+
+ setup: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ return if $(allDeviceSelector.join(",")).length
+
+ # Create all the elements
+ els = $.map BREAKPOINTS, (breakpoint) ->
+ "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
+ $("body").append els.join('')
+
+ visibleDevice: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ $(allDeviceSelector.join(",")).filter(":visible")
+
+ getBreakpointSize: ->
+ $visibleDevice = @visibleDevice
+ # the page refreshed via turbolinks
+ if not $visibleDevice().length
+ @setup()
+ $visibleDevice = @visibleDevice()
+ return $visibleDevice.attr("class").split("visible-")[1]
+
+ @get: ->
+ return instance ?= new BreakpointInstance
+
+$ =>
+ @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index ee81fee5868..1be86e3b820 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -104,6 +104,8 @@ class Dispatcher
new ProjectFork()
when 'projects:artifacts:browse'
new BuildArtifacts()
+ when 'projects:group_links:index'
+ new GroupsSelect()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 8e1449bc59c..4f038477755 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -238,18 +238,20 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+ if !value?
+ field.remove()
+
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
- field.remove()
# Toggle active class for the tick mark
el.toggleClass "is-active"
- if value
+ if value?
if !field.length
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />"
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index cff309c5972..eea3f5ee910 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,8 +1,7 @@
-$(document).on("click", '.toggle-nav-collapse', (e) ->
- e.preventDefault()
- collapsed = 'page-sidebar-collapsed'
- expanded = 'page-sidebar-expanded'
+collapsed = 'page-sidebar-collapsed'
+expanded = 'page-sidebar-expanded'
+toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
niceScrollBars.updateScrollBar();
), 300
+$(document).on("click", '.toggle-nav-collapse', (e) ->
+ e.preventDefault()
+
+ toggleSidebar()
)
+
+$ ->
+ size = bp.getBreakpointSize()
+
+ if size is "xs" or size is "sm"
+ if $('.page-with-sidebar').hasClass(expanded)
+ toggleSidebar()
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 6edabe20136..d20b77ffae9 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -120,6 +120,10 @@
.cover-desc {
padding: 0 $gl-padding 3px;
color: $gl-text-color;
+
+ &.username:last-child {
+ padding-bottom: $gl-padding;
+ }
}
.cover-controls {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index e624982c5c9..4c4033e3ae7 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -141,22 +141,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
-@media (max-width: $screen-md-max) {
- .header-collapsed {
- margin-left: $sidebar_collapsed_width;
- }
-
- .header-expanded {
- margin-left: $sidebar_width;
- }
-}
+.header-collapsed {
+ margin-left: $sidebar_collapsed_width;
-@media(min-width: $screen-md-max) {
- .header-collapsed {
+ @media (min-width: $screen-md-min) {
@include collapsed-header;
}
+}
+
+.header-expanded {
+ margin-left: $sidebar_collapsed_width;
- .header-expanded {
+ @media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 6b382e4b1b2..26df9acd2ae 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -34,12 +34,12 @@
@media (min-width: $screen-sm-min) {
padding-right: $gutter_width;
}
-
+
}
}
.sidebar-wrapper {
- z-index: 99;
+ z-index: 999;
background: $background-color;
}
@@ -203,7 +203,11 @@
}
@mixin expanded-sidebar {
- padding-left: $sidebar_width;
+ padding-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
+ }
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 360930f95a8..06c5c8be9a5 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -46,6 +46,8 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @shared_projects = @group.shared_projects
+
respond_to do |format|
format.html
@@ -133,7 +135,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
end
def load_events
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
new file mode 100644
index 00000000000..4159e53bfa9
--- /dev/null
+++ b/app/controllers/projects/group_links_controller.rb
@@ -0,0 +1,23 @@
+class Projects::GroupLinksController < Projects::ApplicationController
+ layout 'project_settings'
+ before_action :authorize_admin_project!
+
+ def index
+ @group_links = project.project_group_links.all
+ end
+
+ def create
+ link = project.project_group_links.new
+ link.group_id = params[:link_group_id]
+ link.group_access = params[:link_group_access]
+ link.save
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def destroy
+ project.project_group_links.find(params[:id]).destroy
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 8364fc293b7..e7bddc4a6f1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
@project_member = @project.project_members.new
+ @project_group_links = @project.project_group_links
end
def create
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 0e5a8f5ee0f..2c55f088594 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -43,7 +43,8 @@ class ProjectsFinder
if current_user
[
group_projects_for_user(current_user, group),
- group.projects.public_and_internal_only
+ group.projects.public_and_internal_only,
+ group.shared_projects.visible_to_user(current_user)
]
else
[group.projects.public_only]
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1227458e525..7d33838044b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -37,8 +37,6 @@
module Ci
class Build < CommitStatus
- include Gitlab::Application.routes.url_helpers
-
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
@@ -128,7 +126,7 @@ module Ci
end
def retried?
- !self.commit.latest_builds_for_ref(self.ref).include?(self)
+ !self.commit.latest_statuses_for_ref(self.ref).include?(self)
end
def depends_on_builds
@@ -309,22 +307,6 @@ module Ci
project.valid_runners_token? token
end
- def target_url
- namespace_project_build_url(project.namespace, project, self)
- end
-
- def cancel_url
- if active?
- cancel_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
- def retry_url
- if retryable?
- retry_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def can_be_served?(runner)
(tag_list - runner.tag_list).empty?
end
@@ -333,7 +315,7 @@ module Ci
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
- def show_warning?
+ def stuck?
pending? && !any_runners_online?
end
@@ -348,18 +330,6 @@ module Ci
artifacts_file.exists?
end
- def artifacts_download_url
- if artifacts?
- download_namespace_project_build_artifacts_path(project.namespace, project, self)
- end
- end
-
- def artifacts_browse_url
- if artifacts_metadata?
- browse_namespace_project_build_artifacts_path(project.namespace, project, self)
- end
- end
-
def artifacts_metadata?
artifacts? && artifacts_metadata.exists?
end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index ecbd2078b1d..f4cf7034b14 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -25,8 +25,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
-
validates_presence_of :sha
validate :valid_commit_sha
@@ -42,16 +40,6 @@ module Ci
project.id
end
- def last_build
- builds.order(:id).last
- end
-
- def retry
- latest_builds.each do |build|
- Ci::Build.retry(build)
- end
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a
end
- def latest_builds
- @latest_builds ||= builds.latest.to_a
+ def latest_statuses_for_ref(ref)
+ latest_statuses.select { |status| status.ref == ref }
end
- def latest_builds_for_ref(ref)
- latest_builds.select { |build| build.ref == ref }
+ def matrix_builds(build = nil)
+ matrix_builds = builds.latest.ordered
+ matrix_builds = matrix_builds.similar(build) if build
+ matrix_builds.to_a
end
def retried
@@ -170,7 +160,7 @@ module Ci
end
def duration
- duration_array = latest_statuses.map(&:duration).compact
+ duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
@@ -183,16 +173,12 @@ module Ci
end
def coverage
- coverage_array = latest_builds.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
- def matrix_for_ref?(ref)
- latest_builds_for_ref(ref).size > 1
- end
-
def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
@@ -218,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
- def update_committed!
- update!(committed_at: DateTime.now)
- end
-
private
def save_yaml_error(error)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7ef50836322..3b1aa0f5c80 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def cancel_url
- nil
- end
-
- def retry_url
- nil
- end
-
- def show_warning?
+ def stuck?
false
end
-
- def artifacts_download_url
- nil
- end
-
- def artifacts_browse_url
- nil
- end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index afbc2922013..9919ca112dc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -23,6 +23,8 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
+ has_many :project_group_links, dependent: :destroy
+ has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
diff --git a/app/models/note.rb b/app/models/note.rb
index 2e084b5c80c..b0c33f2eec5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -173,26 +173,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end
- # Check if such line of code exists in merge request diff
- # If exists - its active discussion
- # If not - its outdated diff
+ # Check if this note is part of an "active" discussion
+ #
+ # This will always return true for anything except MergeRequest noteables,
+ # which have special logic.
+ #
+ # If the note's current diff cannot be matched in the MergeRequest's current
+ # diff, it's considered inactive.
def active?
return true unless self.diff
return false unless noteable
return @active if defined?(@active)
- diffs = noteable.diffs(Commit.max_diff_options)
- notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
+ noteable_diff = find_noteable_diff
- return @active = false if notable_diff.nil?
+ if noteable_diff
+ parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
- parsed_lines = Gitlab::Diff::Parser.new.parse(notable_diff.diff.each_line)
- # We cannot use ||= because @active may be false
- @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
- end
+ @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
+ else
+ @active = false
+ end
- def outdated?
- !active?
+ @active
end
def diff_file_index
@@ -380,6 +383,12 @@ class Note < ActiveRecord::Base
self.line_code = nil if self.line_code.blank?
end
+ # Find the diff on noteable that matches our own
+ def find_noteable_diff
+ diffs = noteable.diffs(Commit.max_diff_options)
+ diffs.find { |d| d.new_path == self.diff.new_path }
+ end
+
def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 1f18ad78164..79e0cc7b23d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :project_group_links, dependent: :destroy
+ has_many :invited_groups, through: :project_group_links, source: :group
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
@@ -899,6 +901,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active
end
+ def allowed_to_share_with_group?
+ !namespace.share_with_group_lock
+ end
+
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
new file mode 100644
index 00000000000..e52a6bd7c84
--- /dev/null
+++ b/app/models/project_group_link.rb
@@ -0,0 +1,36 @@
+class ProjectGroupLink < ActiveRecord::Base
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MASTER = 40
+
+ belongs_to :project
+ belongs_to :group
+
+ validates :project_id, presence: true
+ validates :group_id, presence: true
+ validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
+ validates :group_access, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
+ validate :different_group
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ DEVELOPER
+ end
+
+ def human_access
+ self.class.access_options.key(self.group_access)
+ end
+
+ private
+
+ def different_group
+ if self.group && self.project && self.project.group == self.group
+ errors.add(:base, "Project cannot be shared with the project it is in.")
+ end
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9629c7e1bb9..70a8bbaba65 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -160,7 +160,27 @@ class ProjectTeam
end
end
- access.max
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ access << max_invited_level(user_id)
+ end
+
+ access.compact.max
+ end
+
+
+ def max_invited_level(user_id)
+ project.project_group_links.map do |group_link|
+ invited_group = group_link.group
+ access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+
+ # If group member has higher access level we should restrict it
+ # to max allowed access level
+ if access && access > group_link.group_access
+ access = group_link.group_access
+ end
+
+ access
+ end.compact.max
end
private
@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
+ invited_members = []
+
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ project.project_group_links.each do |group_link|
+ invited_group = group_link.group
+ im = invited_group.group_members
+
+ if level
+ int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # Skip group members if we ask for masters
+ # but max group access is developers
+ next if int_level > group_link.group_access
+
+ # If we ask for developers and max
+ # group access is developers we need to provide
+ # both group master, developers as devs
+ if int_level == group_link.group_access
+ im.where("access_level >= ?)", group_link.group_access)
+ else
+ im.send(level)
+ end
+ end
+
+ invited_members << im
+ end
+
+ invited_members = invited_members.flatten.compact
+ end
if level
project_members = project_members.send(level)
@@ -175,6 +224,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
+ user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
diff --git a/app/models/user.rb b/app/models/user.rb
index 043bc825ade..8871b0ab9fa 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -832,7 +832,8 @@ class User < ActiveRecord::Base
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
- projects.select(:id)])
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 005a5c4661c..50c95ced8a7 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -3,7 +3,7 @@ module Ci
def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref])
- commit = project.ci_commits.ordered.find_by(sha: sha)
+ commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name)
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 31b407efeb1..69d5c42a877 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -33,7 +33,6 @@ class CreateCommitBuildsService
unless commit.skip_ci?
# Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref)
- commit.update_committed!
commit.create_builds(ref, tag, user)
end
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 34d955568f2..588ad767426 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -4,13 +4,13 @@
= ci_status_with_icon(build.status)
%td.build-link
- - if can?(current_user, :read_build, project) && build.target_url
- = link_to build.target_url do
+ - if can?(current_user, :read_build, build.project)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id}
- else
%strong Build ##{build.id}
- - if build.show_warning?
+ - if build.stuck?
%i.fa.fa-warning.text-warning
%td
@@ -18,11 +18,11 @@
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
%td
- = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
- = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else
.light none
@@ -61,13 +61,12 @@
%td
.pull-right
- if can?(current_user, :read_build, project) && build.artifacts?
- = link_to build.artifacts_download_url, title: 'Download artifacts' do
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
%i.fa.fa-download
- if can?(current_user, :update_build, build.project)
- if build.active?
- - if build.cancel_url
- = link_to build.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && build.retry_url
- = link_to build.retry_url, method: :post, title: 'Retry' do
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
%i.fa.fa-repeat
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f7fd156b84a..264fa1bf0cd 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -50,6 +50,22 @@
.panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
+ - if @group.shared_projects.any?
+ .panel.panel-default
+ .panel-heading
+ Projects shared with #{@group.name}
+ %span.badge
+ #{@group.shared_projects.count}
+ %ul.well-list
+ - @group.shared_projects.sort_by(&:name).each do |project|
+ %li
+ %strong
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ %span.label.label-gray
+ = repository_size(project)
+ %span.pull-right.light
+ %span.monospace= project.path_with_namespace + ".git"
+
.col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
deleted file mode 100644
index 11163813f3e..00000000000
--- a/app/views/ci/commits/_commit.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(commit.status)
- - if commit.running?
- &middot;
- = commit.stage
-
-
- %td.build-link
- = link_to ci_status_path(commit) do
- %strong #{commit.short_sha}
-
- %td.build-message
- %span= truncate_first_line(commit.git_commit_message)
-
- %td.build-branch
- - unless @ref
- %span
- - commit.refs.each do |ref|
- = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
-
- %td.duration
- - if commit.duration > 0
- #{time_interval_in_words commit.duration}
-
- %td.timestamp
- - if commit.finished_at
- %span #{time_ago_in_words commit.finished_at} ago
-
- - if commit.coverage
- %td.coverage
- #{commit.coverage}%
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 7cd8e9bea46..cca7dc27b1c 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,12 +1 @@
-.top-area
- .nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- - if @projects.present?
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- = render 'shared/projects/dropdown'
- - if can? current_user, :create_projects, @group
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
- = icon('plus')
- New Project
-
-= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..b1694c919d0
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1 @@
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 3430f56a9c9..83936d39b16 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -23,6 +23,15 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ .form-group
+ %hr
+ = f.label :share_with_group_lock, class: 'control-label' do
+ Share with group lock
+ .col-sm-10
+ .checkbox
+ = f.check_box :share_with_group_lock
+ %span.descr Prevent sharing a project with another group within this group
+
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 3cf0a4baacd..23a34ac36dd 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -27,18 +27,34 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-
- %ul.nav-links
- %li.active
- = link_to "#projects", 'data-toggle' => 'tab' do
- Projects
-
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
+ .top-area
+ %ul.nav-links
+ %li.active
+ = link_to "#projects", 'data-toggle' => 'tab' do
+ All Projects
+ - if @shared_projects.present?
+ %li
+ = link_to "#shared", 'data-toggle' => 'tab' do
+ Shared Projects
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ = render 'shared/projects/dropdown'
+ - if can? current_user, :create_projects, @group
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
+ = icon('plus')
+ New Project
+
.tab-content
.tab-pane.active#projects
= render "projects", projects: @projects
+ - if @shared_projects.present?
+ .tab-pane#shared
+ = render "shared_projects", projects: @shared_projects
+
- else
%p.nav-links.no-top
No projects to show
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 319974e12c5..0ae83ee01eb 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Project
= nav_link(path: 'projects#activity') do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 3359716202f..dc3050f02e5 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -13,6 +13,12 @@
= icon('pencil-square-o fw')
%span
Project Settings
+ - if @project.allowed_to_share_with_group?
+ = nav_link(controller: :group_links) do
+ = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
+ = icon('share-square-o fw')
+ %span
+ Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 14f1d3226bb..811d304ea75 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -55,7 +55,6 @@
%th Coverage
%th
- - @builds.each do |build|
- = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true
+ = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index be7cc0f256c..b02aee3db21 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -13,9 +13,10 @@
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace
- - if @commit.matrix_for_ref?(@build.ref)
+ - builds = @build.commit.matrix_builds(@build)
+ - if builds.size > 1
%ul.nav-links.no-top.no-bottom
- - @commit.latest_builds_for_ref(@build.ref).each do |build|
+ - builds.each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= ci_icon_for_status(build.status)
@@ -44,7 +45,7 @@
.pull-right
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- - if @build.show_warning?
+ - if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
%p
@@ -100,12 +101,12 @@
%h4.title Build artifacts
.center
.btn-group{ role: :group }
- = link_to @build.artifacts_download_url, class: 'btn btn-sm btn-primary' do
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('download')
Download
- if @build.artifacts_metadata?
- = link_to @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' do
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('folder-open')
Browse
@@ -115,10 +116,10 @@
- if can?(current_user, :update_build, @project)
.center
.btn-group{ role: :group }
- - if @build.cancel_url
- = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
- - elsif @build.retry_url
- = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
+ - elsif @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
- if @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..7123efffd5b
--- /dev/null
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -0,0 +1,77 @@
+%tr.build
+ %td.status
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build), class: "ci-status ci-#{build.status}" do
+ = ci_status_with_icon(build.status)
+ - else
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %strong ##{build.id}
+ - else
+ %strong ##{build.id}
+
+ - if build.stuck?
+ %i.fa.fa-warning.text-warning
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if build.try(:runner)
+ = runner_link(build.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = build.stage
+
+ %td
+ = build.name
+
+ .pull-right
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_with_tooltip(build.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if build.try(:coverage)
+ #{build.coverage}%
+
+ %td
+ .pull-right
+ - if can?(current_user, :read_build, build) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if can?(current_user, :update_build, build)
+ - if build.active?
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
+ %i.fa.fa-repeat
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index befad27666c..003b7c18d0e 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -43,8 +43,8 @@
%th Coverage
%th
- @ci_commit.refs.each do |ref|
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
+ - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
+ = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
- if @ci_commit.retried.any?
.gray-content-block.second-block
@@ -64,5 +64,4 @@
- if @ci_commit.project.build_coverage_enabled?
%th Coverage
%th
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
+ = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
deleted file mode 100644
index a3449d1ae05..00000000000
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ /dev/null
@@ -1,79 +0,0 @@
-%tr.commit_status
- %td.status
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
- = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
- = ci_icon_for_status(commit_status.status)
- = commit_status.status
- - else
- = ci_status_with_icon(commit_status.status)
-
- %td.commit_status-link
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
- = link_to commit_status.target_url do
- %strong ##{commit_status.id}
- - else
- %strong ##{commit_status.id}
-
- - if commit_status.show_warning?
- %i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"}
-
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
-
- %td
- - if commit_status.ref
- = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
- - else
- .light none
-
- - if defined?(runner) && runner
- %td
- - if commit_status.try(:runner)
- = runner_link(commit_status.runner)
- - else
- .light none
-
- - if defined?(stage) && stage
- %td
- = commit_status.stage
-
- %td
- = commit_status.name
-
- .pull-right
- - if commit_status.tags.any?
- - commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if commit_status.try(:trigger_request)
- %span.label.label-info triggered
- - if commit_status.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if commit_status.duration
- #{duration_in_words(commit_status.finished_at, commit_status.started_at)}
-
- %td.timestamp
- - if commit_status.finished_at
- %span #{time_ago_with_tooltip(commit_status.finished_at)}
-
- - if defined?(coverage) && coverage
- %td.coverage
- - if commit_status.try(:coverage)
- #{commit_status.coverage}%
-
- %td
- .pull-right
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url
- = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
- %i.fa.fa-download
- - if can?(current_user, :update_commit_status, commit_status)
- - if commit_status.active?
- - if commit_status.cancel_url
- = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
- = link_to commit_status.retry_url, method: :post, title: 'Retry' do
- %i.fa.fa-repeat
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..4143ea13063
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1,59 @@
+%tr.generic_commit_status
+ %td.status
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = link_to generic_commit_status.target_url, class: "ci-status ci-#{generic_commit_status.status}" do
+ = ci_status_with_icon(generic_commit_status.status)
+ - else
+ = ci_status_with_icon(generic_commit_status.status)
+
+ %td.generic_commit_status-link
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = link_to generic_commit_status.target_url do
+ %strong ##{generic_commit_status.id}
+ - else
+ %strong ##{generic_commit_status.id}
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+
+ %td
+ - if generic_commit_status.ref
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if generic_commit_status.try(:runner)
+ = runner_link(generic_commit_status.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = generic_commit_status.stage
+
+ %td
+ = generic_commit_status.name
+
+ .pull-right
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ %td.duration
+ - if generic_commit_status.duration
+ #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
+
+ %td.timestamp
+ - if generic_commit_status.finished_at
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
new file mode 100644
index 00000000000..13f5fc141fa
--- /dev/null
+++ b/app/views/projects/group_links/index.html.haml
@@ -0,0 +1,41 @@
+- page_title "Groups"
+%h3.page_title Share project with other groups
+%p.light
+ Projects can be stored in only one group at once. However you can share a project with other groups here.
+%hr
+- if @group_links.present?
+ .enabled-groups.panel.panel-default
+ .panel-heading
+ Already shared with
+ %ul.well-list
+ - @group_links.each do |group_link|
+ - group = group_link.group
+ %li
+ .pull-right
+ = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
+ %i.icon-remove
+ disable sharing
+ = link_to group do
+ %strong
+ %i.icon-folder-open
+ = group.name
+ %br
+ .light up to #{group_link.human_access}
+
+
+.available-groups
+ %h4
+ Can be shared with
+ %div
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
+ .form-group
+ = label_tag :link_group_id, 'Group', class: 'control-label'
+ .col-sm-10
+ = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ .form-group
+ = label_tag :link_group_access, 'Max access level', class: 'control-label'
+ .col-sm-10
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
+ .form-actions
+ = submit_tag "Share", class: "btn btn-create"
+
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
new file mode 100644
index 00000000000..62888e41935
--- /dev/null
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -0,0 +1,21 @@
+- @project_group_links.each do |group_links|
+ - shared_group = group_links.group
+ - shared_group_users_count = group_links.group.group_members.count
+ .panel.panel-default
+ .panel-heading
+ Shared with
+ %strong #{shared_group.name}
+ group, members with
+ %strong #{group_links.human_access}
+ role (#{shared_group_users_count})
+ - if current_user.can?(:admin_group, shared_group)
+ .panel-head-actions
+ = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit group members
+ %ul.content-list
+ - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ - if shared_group_users_count > 20
+ %li
+ and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 0f8848a5cbe..ebcfc907ebb 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -18,3 +18,6 @@
- if @group
= render "group_members", members: @group_members
+
+ - if @project_group_links.any? && @project.allowed_to_share_with_group?
+ = render "shared_group_members"
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index e62d80aeb2c..42a3c2c3f02 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -15,7 +15,7 @@
.filter-item.inline
- if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id])
- = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
@@ -94,7 +94,7 @@
%a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
diff --git a/config/routes.rb b/config/routes.rb
index 869fca03ec4..536397e318a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -701,6 +701,8 @@ Rails.application.routes.draw do
end
end
+ resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
+
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do
delete :delete_attachment
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
new file mode 100644
index 00000000000..395083f2a03
--- /dev/null
+++ b/db/migrate/20130711063759_create_project_group_links.rb
@@ -0,0 +1,10 @@
+class CreateProjectGroupLinks < ActiveRecord::Migration
+ def change
+ create_table :project_group_links do |t|
+ t.integer :project_id, null: false
+ t.integer :group_id, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
new file mode 100644
index 00000000000..00e3947a6bb
--- /dev/null
+++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb
@@ -0,0 +1,5 @@
+class AddAccessToProjectGroupLink < ActiveRecord::Migration
+ def change
+ add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
+ end
+end
diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb
new file mode 100644
index 00000000000..78d1a4538f2
--- /dev/null
+++ b/db/migrate/20150930110012_add_group_share_lock.rb
@@ -0,0 +1,5 @@
+class AddGroupShareLock < ActiveRecord::Migration
+ def change
+ add_column :namespaces, :share_with_group_lock, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cf3f8245b38..5027d2ba32f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -578,14 +578,15 @@ ActiveRecord::Schema.define(version: 20160314143402) do
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ t.string "description", default: "", null: false
t.string "avatar"
+ t.boolean "share_with_group_lock", default: false
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
@@ -669,6 +670,14 @@ ActiveRecord::Schema.define(version: 20160314143402) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "project_group_links", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "group_access", default: 30, null: false
+ end
+
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
@@ -766,9 +775,9 @@ ActiveRecord::Schema.define(version: 20160314143402) do
t.string "type"
t.string "title"
t.integer "project_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "active", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
t.text "properties"
t.boolean "template", default: false
t.boolean "push_events", default: true
diff --git a/doc/api/builds.md b/doc/api/builds.md
index d3ce72e59fc..4c0a47d1ea0 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -33,7 +33,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.802Z",
- "download_url": null,
"artifacts_file": {
"filename": "artifacts.zip",
"size": 1000
@@ -75,7 +74,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.727Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
@@ -139,7 +137,6 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
@@ -164,7 +161,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.957Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:33.913Z",
"id": 9,
@@ -226,7 +222,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.880Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z",
"id": 8,
@@ -315,7 +310,6 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
@@ -362,7 +356,6 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "download_url": null,
"artifacts_file": null,
"finished_at": null,
"id": 69,
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 85d4f0bafa2..d4d63e825ab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -145,6 +145,7 @@ Parameters:
"state": "active",
"created_at": "2013-09-30T13:46:01Z"
},
+ "expires_at": null,
"updated_at": "2013-10-02T07:34:20Z",
"created_at": "2013-10-02T07:34:20Z"
}
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index fb802102e3a..a7acf37b5bc 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -51,6 +51,7 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
+ "expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z"
}
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 9e9486cd87a..3703f4b327a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -619,6 +619,20 @@ Revoking team membership for a user who is not currently a team member is consid
Please note that the returned JSON currently differs slightly. Thus you should not
rely on the returned JSON structure.
+### Share project with group
+
+Allow to share project with group.
+
+```
+POST /projects/:id/share
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `group_id` (required) - The ID of a group
+- `group_access` (required) - Level of permissions for sharing
+
## Hooks
Also called Project Hooks and Webhooks.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 051eaa04152..5158e3c387c 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -116,7 +116,8 @@ Alias for [stages](#stages).
### variables
-_**Note:** Introduced in GitLab Runner v0.5.0._
+>**Note:**
+Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
environment. The variables are stored in the git repository and are meant to
@@ -153,7 +154,8 @@ cache:
#### cache:key
-_**Note:** Introduced in GitLab Runner v1.0.0._
+>**Note:**
+Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs,
@@ -234,13 +236,14 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
-| stage | no (default: `test`) | Defines a build stage |
+| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
@@ -393,15 +396,18 @@ The above script will:
### artifacts
-_**Note:** Introduced in GitLab Runner v0.7.0 for non-Windows platforms._
-
-_**Note:** Limited Windows support was added in GitLab Runner v.1.0.0.
-Currently not all executors are supported._
-
-_**Note:** Build artifacts are only collected for successful builds._
+>**Notes:**
+>
+> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
+> - Windows support was added in GitLab Runner v.1.0.0.
+> - Currently not all executors are supported.
+> - Build artifacts are only collected for successful builds.
`artifacts` is used to specify list of files and directories which should be
-attached to build after success. Below are some examples.
+attached to build after success. To pass artifacts between different builds,
+see [dependencies](#dependencies).
+
+Below are some examples.
Send all files in `binaries` and `.config`:
@@ -453,9 +459,130 @@ release-job:
The artifacts will be sent to GitLab after a successful build and will
be available for download in the GitLab UI.
+#### artifacts:name
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
+
+The `name` directive allows you to define the name of the created artifacts
+archive. That way, you can have a unique name of every archive which could be
+useful when you'd like to download the archive from GitLab. The `artifacts:name`
+variable can make use of any of the [predefined variables](../variables/README.md).
+
+---
+
+**Example configurations**
+
+To create an archive with a name of the current build:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_BUILD_NAME"
+```
+
+To create an archive with a name of the current branch or tag including only
+the files that are untracked by Git:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_BUILD_REF_NAME"
+ untracked: true
+```
+
+To create an archive with a name of the current build and the current branch or
+tag including only the files that are untracked by Git:
+
+```yaml
+job:
+ artifacts:
+ name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
+ untracked: true
+```
+
+To create an archive with a name of the current [stage](#stages) and branch name:
+
+```yaml
+job:
+ artifacts:
+ name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
+ untracked: true
+```
+
+---
+
+If you use **Windows Batch** to run your shell scripts you need to replace
+`$` with `%`:
+
+```yaml
+job:
+ artifacts:
+ name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
+ untracked: true
+```
+
+### dependencies
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+This feature should be used in conjunction with [`artifacts`](#artifacts) and
+allows you to define the artifacts to pass between different builds.
+
+Note that `artifacts` from previous [stages](#stages) are passed by default.
+
+To use this feature, define `dependencies` in context of the job and pass
+a list of all previous builds from which the artifacts should be downloaded.
+You can only define builds from stages that are executed before the current one.
+An error will be shown if you define builds from the current stage or next ones.
+
+---
+
+In the following example, we define two jobs with artifacts, `build:osx` and
+`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
+will be downloaded and extracted in the context of the build. The same happens
+for `test:linux` and artifacts from `build:linux`.
+
+The job `deploy` will download artifacts from all previous builds because of
+the [stage](#stages) precedence:
+
+```yaml
+build:osx:
+ stage: build
+ script: make build:osx
+ artifacts:
+ paths:
+ - binaries/
+
+build:linux:
+ stage: build
+ script: make build:linux
+ artifacts:
+ paths:
+ - binaries/
+
+test:osx:
+ stage: test
+ script: make test:osx
+ dependencies:
+ - build:osx
+
+test:linux:
+ stage: test
+ script: make test:linux
+ dependencies:
+ - build:linux
+
+deploy:
+ stage: deploy
+ script: make deploy
+```
+
### cache
-_**Note:** Introduced in GitLab Runner v0.7.0._
+>**Note:**
+Introduced in GitLab Runner v0.7.0.
`cache` is used to specify list of files and directories which should be cached
between builds. Below are some examples:
@@ -509,6 +636,155 @@ rspec:
The cache is provided on best effort basis, so don't expect that cache will be
always present. For implementation details please check GitLab Runner.
+## Hidden jobs
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
+use this feature to ignore jobs, or use the
+[special YAML features](#special-yaml-features) and transform the hidden jobs
+into templates.
+
+In the following example, `.job_name` will be ignored:
+
+```yaml
+.job_name:
+ script:
+ - rake spec
+```
+
+## Special YAML features
+
+It's possible to use special YAML features like anchors (`&`), aliases (`*`)
+and map merging (`<<`), which will allow you to greatly reduce the complexity
+of `.gitlab-ci.yml`.
+
+Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
+
+### Anchors
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+YAML also has a handy feature called 'anchors', which let you easily duplicate
+content across your document. Anchors can be used to duplicate/inherit
+properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
+to provide templates for your jobs.
+
+The following example uses anchors and map merging. It will create two jobs,
+`test1` and `test2`, that will inherit the parameters of `.job_template`, each
+having their own custom `script` defined:
+
+```yaml
+.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+
+test1:
+ <<: *job_definition # Merge the contents of the 'job_definition' alias
+ script:
+ - test1 project
+
+test2:
+ <<: *job_definition # Merge the contents of the 'job_definition' alias
+ script:
+ - test2 project
+```
+
+`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
+given hash into the current one", and `*` includes the named anchor
+(`job_definition` again). The expanded version looks like this:
+
+```yaml
+.job_template:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+
+test1:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+ script:
+ - test1 project
+
+test2:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+ script:
+ - test2 project
+```
+
+Let's see another one example. This time we will use anchors to define two sets
+of services. This will create two jobs, `test:postgres` and `test:mysql`, that
+will share the `script` directive defined in `.job_template`, and the `services`
+directive defined in `.postgres_services` and `.mysql_services` respectively:
+
+```yaml
+.job_template: &job_definition
+ script:
+ - test project
+
+.postgres_services:
+ services: &postgres_definition
+ - postgres
+ - ruby
+
+.mysql_services:
+ services: &mysql_definition
+ - mysql
+ - ruby
+
+test:postgres:
+ << *job_definition
+ services: *postgres_definition
+
+test:mysql:
+ << *job_definition
+ services: *mysql_definition
+```
+
+The expanded version looks like this:
+
+```yaml
+.job_template:
+ script:
+ - test project
+
+.postgres_services:
+ services:
+ - postgres
+ - ruby
+
+.mysql_services:
+ services:
+ - mysql
+ - ruby
+
+test:postgres:
+ script:
+ - test project
+ services:
+ - postgres
+ - ruby
+
+test:mysql:
+ script:
+ - test project
+ services:
+ - mysql
+ - ruby
+```
+
+You can see that the hidden jobs are conveniently used as templates.
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 0fd54be58b0..4f011397269 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -233,9 +233,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-5-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
-**Note:** You can change `8-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
new file mode 100644
index 00000000000..024f6e8a433
--- /dev/null
+++ b/doc/update/8.5-to-8.6.md
@@ -0,0 +1,164 @@
+# From 8.5 to 8.6
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-6-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-6-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.11
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout 0.6.5
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-5-stable:config/gitlab.yml.example origin/8-6-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-5-stable:lib/support/nginx/gitlab-ssl origin/8-6-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-5-stable:lib/support/nginx/gitlab origin/8-6-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-6-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+### 8. Updates for PostgreSQL Users
+
+Starting with 8.6 users using GitLab in combination with PostgreSQL are required
+to have the `pg_trgm` extension enabled for all GitLab databases. If you're
+using GitLab's Omnibus packages there's nothing you'll need to do manually as
+this extension is enabled automatically. Users who install GitLab without using
+Omnibus (e.g. by building from source) have to enable this extension manually.
+To enable this extension run the following SQL command as a PostgreSQL super
+user for _every_ GitLab database:
+
+```sql
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
+```
+
+Certain operating systems might require the installation of extra packages for
+this extension to be available. For example, users using Ubuntu will have to
+install the `postgresql-contrib` package in order for this extension to be
+available.
+
+### 9. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.4 to 8.5](8.4-to-8.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 87049427139..afdf1a682e2 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -582,6 +582,7 @@ X-Gitlab-Event: Note Hook
"created_at": "2015-04-09 02:40:38 UTC",
"updated_at": "2015-04-09 02:40:38 UTC",
"file_name": "test.rb",
+ "expires_at": null,
"type": "ProjectSnippet",
"visibility_level": 0
}
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 2ac32373ce9..25893f948ea 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,6 +13,8 @@
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
+- [Sharing a project with a group](share_with_group.md)
+- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png
new file mode 100644
index 00000000000..71106a8a5a0
--- /dev/null
+++ b/doc/workflow/groups/max_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png
new file mode 100644
index 00000000000..cbf2c3c1fdc
--- /dev/null
+++ b/doc/workflow/groups/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
new file mode 100644
index 00000000000..a5dbc89fe90
--- /dev/null
+++ b/doc/workflow/groups/share_project_with_groups.png
Binary files differ
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
new file mode 100644
index 00000000000..4c59f59c587
--- /dev/null
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -0,0 +1,30 @@
+# Share Projects with other Groups
+
+In GitLab Enterprise Edition you can share projects with other groups.
+This makes it possible to add a group of users to a project with a single action.
+
+## Groups as collections of users
+
+In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
+In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+
+## Sharing a project with a group of users
+
+The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
+But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the (Enterprise Edition only) group sharing feature can be of use.
+
+To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
+
+![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+
+Now you can add the 'Engineering' group with the maximum access level of your choice.
+After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
+
+!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png)
+
+## Maximum access level
+
+!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png)
+
+In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md
new file mode 100644
index 00000000000..3b7690973cb
--- /dev/null
+++ b/doc/workflow/share_with_group.md
@@ -0,0 +1,13 @@
+# Sharing a project with a group
+
+If you want to share a single project in a group with another group,
+you can do so easily. By setting the permission you can quickly
+give a select group of users access to a project in a restricted manner.
+
+In a project go to the project settings -> groups.
+
+Now you can select a group that you want to share this project with and with
+which maximum access level. Users in that group are able to access this project
+with their set group access level, up to the maximum level that you've set.
+
+![Share a project with a group](share_with_group.png)
diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png
new file mode 100644
index 00000000000..a0ca6f14552
--- /dev/null
+++ b/doc/workflow/share_with_group.png
Binary files differ
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index 2edb3964f70..ab7de7ac315 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -21,6 +21,11 @@ Feature: Admin Groups
When I select user "John Doe" from user list as "Reporter"
Then I should see "John Doe" in team list in every project as "Reporter"
+ Scenario: Shared projects
+ Given group has shared projects
+ When I visit group page
+ Then I should see project shared with group
+
@javascript
Scenario: Remove user from group
Given we have user "John Doe" in group
diff --git a/features/project/group_links.feature b/features/project/group_links.feature
new file mode 100644
index 00000000000..2657c4487ad
--- /dev/null
+++ b/features/project/group_links.feature
@@ -0,0 +1,16 @@
+Feature: Project Group Links
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" is shared with group "Ops"
+ And project "Shop" is not shared with group "Market"
+ And I visit project group links page
+
+ Scenario: I should see list of groups
+ Then I should see project already shared with group "Ops"
+ Then I should see project is not shared with group "Market"
+
+ @javascript
+ Scenario: I share project with group
+ When I select group "Market" for share
+ Then I should see project is shared with group "Market"
diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature
index 6cc89a15a78..89a02706bd2 100644
--- a/features/project/network_graph.feature
+++ b/features/project/network_graph.feature
@@ -34,9 +34,10 @@ Feature: Project Network Graph
@javascript
Scenario: I should filter selected tag
When I switch ref to "v1.0.0"
+ Then page should have "v1.0.0" in title
Then page should have content not containing "v1.0.0"
When click "Show only selected branch" checkbox
- Then page should not have content not containing "v1.0.0"
+ Then page should only have content from "v1.0.0"
When click "Show only selected branch" checkbox
Then page should have content not containing "v1.0.0"
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 06fb45c8bde..5888662fc3f 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -39,3 +39,8 @@ Feature: Project Team Management
And I click link "Import team from another project"
And I submit "Website" project for import team
Then I should see "Mike" in team list as "Reporter"
+
+ Scenario: See all members of projects shared group
+ Given I share project with group "OpenSource"
+ And I visit project "Shop" team page
+ Then I should see "Opensource" group user listing
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index 43fd91d0d4c..e1f1db2872f 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
end
end
+ step 'group has shared projects' do
+ share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = current_group.id
+ share_link.save!
+ end
+
+ step 'I visit group page' do
+ visit admin_group_path(current_group)
+ end
+
+ step 'I should see project shared with group' do
+ expect(page).to have_content(shared_project.name_with_namespace)
+ expect(page).to have_content "Projects shared with"
+ end
+
step 'we have user "John Doe" in group' do
current_group.add_reporter(user_john)
end
@@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
@group ||= Group.first
end
+ def shared_project
+ @shared_project ||= create(:empty_project)
+ end
+
def user_john
@user_john ||= User.find_by(name: "John Doe")
end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 3072c790a4a..f4a56865532 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -36,11 +36,9 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- execute_script('$("#assignee_id").val("")')
- execute_script('$(".js-user-search").first().click()')
- sleep 1
- execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
- sleep 1
+ find("#assignee_id").set("")
+ find(".js-author-search", match: :first).click
+ find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 7fc0e444e86..a2adc87f8ef 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -40,22 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- execute_script('$("#assignee_id").val("")')
- execute_script('$(".js-user-search").first().click()')
- sleep 0.5
- execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
- sleep 2
+ find("#assignee_id").set("")
+ find(".js-author-search", match: :first).click
+ find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
- execute_script('$(".js-user-search").first().click()')
- sleep 0.5
- execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
- sleep 2
- execute_script('$(".js-user-search").eq(1).click()')
- sleep 0.5
- execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
- sleep 2
+ find(".js-author-search").click
+ find(".dropdown-menu-author li a", match: :first).click
+ find(".js-assignee-search").click
+ find(".dropdown-menu-assignee li a", match: :first).click
end
def should_see(merge_request)
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 135e1d016ae..ce2554bc80d 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -13,7 +13,6 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
- sleep 0.3
end
end
@@ -46,12 +45,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I have award added' do
- sleep 0.2
-
page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
- expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
end
end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 7a83d32a240..9b59b682676 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
When 'I switch ref to "feature"' do
select 'feature', from: 'ref'
- sleep 2
end
When 'I switch ref to "v1.0.0"' do
select 'v1.0.0', from: 'ref'
- sleep 2
end
When 'click "Show only selected branch" checkbox' do
find('#filter_ref').click
- sleep 2
end
step 'page should have content not containing "v1.0.0"' do
@@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
end
- step 'page should not have content not containing "v1.0.0"' do
+ step 'page should have "v1.0.0" in title' do
+ expect(page).to have_css 'title', text: 'Network ยท v1.0.0', visible: false
+ end
+
+ step 'page should only have content from "v1.0.0"' do
page.within '.network-graph' do
expect(page).not_to have_content 'Change some files'
end
diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb
new file mode 100644
index 00000000000..739a85e5fa4
--- /dev/null
+++ b/features/steps/project/project_group_links.rb
@@ -0,0 +1,50 @@
+class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include Select2Helper
+
+ step 'I should see project already shared with group "Ops"' do
+ page.within '.enabled-groups' do
+ expect(page).to have_content "Ops"
+ end
+ end
+
+ step 'I should see project is not shared with group "Market"' do
+ page.within '.enabled-groups' do
+ expect(page).not_to have_content "Market"
+ end
+ end
+
+ step 'I select group "Market" for share' do
+ group = Group.find_by(path: 'market')
+ select2(group.id, from: "#link_group_id")
+ select "Master", from: 'link_group_access'
+ click_button "Share"
+ end
+
+ step 'I should see project is shared with group "Market"' do
+ page.within '.enabled-groups' do
+ expect(page).to have_content "Market"
+ end
+ end
+
+ step 'project "Shop" is shared with group "Ops"' do
+ group = create(:group, name: 'Ops')
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = group.id
+ share_link.save!
+ end
+
+ step 'project "Shop" is not shared with group "Market"' do
+ create(:group, name: 'Market', path: 'market')
+ end
+
+ step 'I visit project group links page' do
+ visit namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def project
+ @project ||= Project.find_by_name "Shop"
+ end
+end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index caad52def79..3fbcf770b62 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_link('Remove user from team')
end
end
+
+ step 'I share project with group "OpenSource"' do
+ project = Project.find_by(name: 'Shop')
+ os_group = create(:group, name: 'OpenSource')
+ create(:project, group: os_group)
+ @os_user1 = create(:user)
+ @os_user2 = create(:user)
+ os_group.add_owner(@os_user1)
+ os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER)
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = os_group.id
+ share_link.save!
+ end
+
+ step 'I should see "Opensource" group user listing' do
+ expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
+ expect(page).to have_content(@os_user1.name)
+ expect(page).to have_content(@os_user2.name)
+ end
end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index f33ed7834fe..c4c7672a432 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -68,7 +68,7 @@ module SharedBuilds
end
step 'I see the build' do
- page.within('.commit_status') do
+ page.within('.build') do
expect(page).to have_content "##{@build.id}"
expect(page).to have_content @build.sha[0..7]
expect(page).to have_content @build.ref
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5b5b8bd044b..9805e53624e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -144,6 +144,9 @@ module API
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
+
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ expose(:expires_at) { |snippet| nil }
end
class ProjectEntity < Grape::Entity
@@ -243,6 +246,10 @@ module API
end
end
+ class ProjectGroupLink < Grape::Entity
+ expose :id, :project_id, :group_id, :group_access
+ end
+
class Namespace < Grape::Entity
expose :id, :path, :kind
end
@@ -401,13 +408,6 @@ module API
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
- # TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API
- # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255)
- expose :download_url do |repo_obj, options|
- if options[:user_can_download_artifacts]
- repo_obj.artifacts_download_url
- end
- end
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: RepoCommit do |repo_obj, _options|
if repo_obj.respond_to?(:commit)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6067c8b4a5e..6fcb5261e40 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -290,6 +290,33 @@ module API
end
end
+ # Share project with group
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # group_id (required) - The ID of a group
+ # group_access (required) - Level of permissions for sharing
+ #
+ # Example Request:
+ # POST /projects/:id/share
+ post ":id/share" do
+ authorize! :admin_project, user_project
+ required_attributes! [:group_id, :group_access]
+
+ unless user_project.allowed_to_share_with_group?
+ return render_api_error!("The project sharing with group is disabled", 400)
+ end
+
+ link = user_project.project_group_links.new
+ link.group_id = params[:group_id]
+ link.group_access = params[:group_access]
+ if link.save
+ present link, with: Entities::ProjectGroupLink
+ else
+ render_api_error!(link.errors.full_messages.first, 409)
+ end
+ end
+
# Upload a file
#
# Parameters:
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index abd79b329ae..e8011519608 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,7 +7,7 @@ module Banzai
#
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
- UNSAFE_PROTOCOLS = %w(javascript :javascript data vbscript).freeze
+ UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist
whitelist = super
@@ -64,7 +64,12 @@ module Banzai
return unless node.name == 'a'
return unless node.has_attribute?('href')
- if node['href'].start_with?(*UNSAFE_PROTOCOLS)
+ begin
+ uri = Addressable::URI.parse(node['href'])
+ uri.scheme.strip! if uri.scheme
+
+ node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
node.remove_attribute('href')
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 28e074cd289..c89e1b51019 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -5,7 +5,9 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
- ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
+ :allow_failure, :type, :stage, :when, :artifacts, :cache,
+ :dependencies]
attr_reader :before_script, :image, :services, :variables, :path, :cache
@@ -60,6 +62,7 @@ module Ci
@jobs = {}
@config.each do |key, job|
+ next if key.to_s.start_with?('.')
stage = job[:stage] || job[:type] || DEFAULT_STAGE
@jobs[key] = { stage: stage }.merge(job)
end
@@ -81,6 +84,7 @@ module Ci
services: job[:services] || @services,
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
+ dependencies: job[:dependencies],
}.compact
}
end
@@ -143,6 +147,7 @@ module Ci
validate_job_stage!(name, job) if job[:stage]
validate_job_cache!(name, job) if job[:cache]
validate_job_artifacts!(name, job) if job[:artifacts]
+ validate_job_dependencies!(name, job) if job[:dependencies]
end
private
@@ -216,6 +221,10 @@ module Ci
end
def validate_job_artifacts!(name, job)
+ if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
+ raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
+ end
+
if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
end
@@ -225,6 +234,22 @@ module Ci
end
end
+ def validate_job_dependencies!(name, job)
+ if !validate_array_of_strings(job[:dependencies])
+ raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
+ end
+
+ stage_index = stages.index(job[:stage])
+
+ job[:dependencies].each do |dependency|
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
+
+ unless stages.index(@jobs[dependency][:stage]) < stage_index
+ raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
+ end
+ end
+ end
+
def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
new file mode 100644
index 00000000000..e73cc05f9d7
--- /dev/null
+++ b/spec/factories/project_group_links.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :project_group_link do
+ project
+ group
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index f32641ef0f6..fae0da9d898 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -17,6 +17,10 @@ describe ProjectsFinder do
create(:project, :public, group: group, name: 'C', path: 'C')
end
+ let!(:shared_project) do
+ create(:project, :private, name: 'D', path: 'D')
+ end
+
let(:finder) { described_class.new }
describe 'without a group' do
@@ -56,7 +60,35 @@ describe ProjectsFinder do
describe 'with a user' do
subject { finder.execute(user, group: group) }
- it { is_expected.to eq([public_project, internal_project]) }
+ describe 'without shared projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+
+ describe 'with shared projects and group membership' do
+ before do
+ group.add_user(user, Gitlab::Access::DEVELOPER)
+
+ shared_project.project_group_links.
+ create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+ it do
+ is_expected.to eq([shared_project, public_project, internal_project])
+ end
+ end
+
+ describe 'with shared projects and project membership' do
+ before do
+ shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+
+ shared_project.project_group_links.
+ create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+ it do
+ is_expected.to eq([shared_project, public_project, internal_project])
+ end
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 4a7b00c7660..27ce312b11c 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -149,10 +149,20 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
output: '<a href="java"></a>'
},
+ 'protocol-based JS injection: invalid URL char' => {
+ input: '<img src=java\script:alert("XSS")>',
+ output: '<img>'
+ },
+
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>'
},
+
+ 'protocol whitespace' => {
+ input: '<a href=" http://example.com/"></a>',
+ output: '<a href="http://example.com/"></a>'
+ }
}
protocols.each do |name, data|
@@ -177,6 +187,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(output.to_html).to eq '<a>XSS</a>'
end
+ it 'disallows invalid URIs' do
+ expect(Addressable::URI).to receive(:parse).with('foo://example.com').
+ and_raise(Addressable::URI::InvalidURIError)
+
+ input = '<a href="foo://example.com">Foo</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>Foo</a>'
+ end
+
it 'allows non-standard anchor schemes' do
exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
act = filter(exp)
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 1e98280d045..fab6412d29f 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -397,7 +397,7 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
- artifacts: { paths: ["logs/", "binaries/"], untracked: true },
+ artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
script: "rspec"
}
})
@@ -417,6 +417,7 @@ module Ci
image: "ruby:2.1",
services: ["mysql"],
artifacts: {
+ name: "custom_name",
paths: ["logs/", "binaries/"],
untracked: true
}
@@ -427,6 +428,73 @@ module Ci
end
end
+ describe "Dependencies" do
+ let(:config) do
+ {
+ build1: { stage: 'build', script: 'test' },
+ build2: { stage: 'build', script: 'test' },
+ test1: { stage: 'test', script: 'test', dependencies: dependencies },
+ test2: { stage: 'test', script: 'test' },
+ deploy: { stage: 'test', script: 'test' }
+ }
+ end
+
+ subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ context 'no dependencies' do
+ let(:dependencies) { }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'dependencies to builds' do
+ let(:dependencies) { [:build1, :build2] }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'undefined dependency' do
+ let(:dependencies) { [:undefined] }
+
+ it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
+ end
+
+ context 'dependencies to deploy' do
+ let(:dependencies) { [:deploy] }
+
+ it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
+ end
+ end
+
+ describe "Hidden jobs" do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { script: 'test' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ let(:config_processor) { GitlabCiYamlProcessor.new(config) }
+
+ subject { config_processor.builds_for_stage_and_ref("test", "master") }
+
+ it "doesn't create jobs that starts with dot" do
+ expect(subject.size).to eq(1)
+ expect(subject.first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :normal_job,
+ only: nil,
+ commands: "\ntest",
+ tag_list: [],
+ options: {},
+ when: "on_success",
+ allow_failure: false
+ })
+ end
+ end
+
describe "YAML Alias/Anchor" do
it "is correctly supported for jobs" do
config = <<EOT
@@ -629,6 +697,13 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end
+ it "returns errors if job artifacts:name is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
+ end
+
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
@@ -684,6 +759,13 @@ EOT
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
end
+
+ it "returns errors if job dependencies is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
+ end
end
end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index e3d3d453653..b7457808040 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -9,7 +9,7 @@ describe Ci::Build, models: true do
it { is_expected.to respond_to :trace_html }
- describe :first_pending do
+ describe '#first_pending' do
let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
before { first; second }
@@ -19,7 +19,7 @@ describe Ci::Build, models: true do
it('returns with the first pending build') { is_expected.to eq(first) }
end
- describe :create_from do
+ describe '#create_from' do
before do
build.status = 'success'
build.save
@@ -33,7 +33,7 @@ describe Ci::Build, models: true do
end
end
- describe :ignored? do
+ describe '#ignored?' do
subject { build.ignored? }
context 'if build is not allowed to fail' do
@@ -69,7 +69,7 @@ describe Ci::Build, models: true do
end
end
- describe :trace do
+ describe '#trace' do
subject { build.trace_html }
it { is_expected.to be_empty }
@@ -101,7 +101,7 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(commit.project.timeout) }
# end
- describe :options do
+ describe '#options' do
let(:options) do
{
image: "ruby:2.1",
@@ -122,25 +122,25 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(project.allow_git_fetch) }
# end
- describe :project do
+ describe '#project' do
subject { build.project }
it { is_expected.to eq(commit.project) }
end
- describe :project_id do
+ describe '#project_id' do
subject { build.project_id }
it { is_expected.to eq(commit.project_id) }
end
- describe :project_name do
+ describe '#project_name' do
subject { build.project_name }
it { is_expected.to eq(project.name) }
end
- describe :extract_coverage do
+ describe '#extract_coverage' do
context 'valid content & regex' do
subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
@@ -172,7 +172,7 @@ describe Ci::Build, models: true do
end
end
- describe :variables do
+ describe '#variables' do
context 'returns variables' do
subject { build.variables }
@@ -242,7 +242,7 @@ describe Ci::Build, models: true do
end
end
- describe :can_be_served? do
+ describe '#can_be_served?' do
let(:runner) { FactoryGirl.create :ci_runner }
before { build.project.runners << runner }
@@ -277,7 +277,7 @@ describe Ci::Build, models: true do
end
end
- describe :any_runners_online? do
+ describe '#any_runners_online?' do
subject { build.any_runners_online? }
context 'when no runners' do
@@ -312,8 +312,8 @@ describe Ci::Build, models: true do
end
end
- describe :show_warning? do
- subject { build.show_warning? }
+ describe '#stuck?' do
+ subject { build.stuck? }
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
@@ -343,35 +343,7 @@ describe Ci::Build, models: true do
end
end
- describe :artifacts_download_url do
- subject { build.artifacts_download_url }
-
- context 'artifacts file does not exist' do
- before { build.update_attributes(artifacts_file: nil) }
- it { is_expected.to be_nil }
- end
-
- context 'artifacts file exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to_not be_nil }
- end
- end
-
- describe :artifacts_browse_url do
- subject { build.artifacts_browse_url }
-
- it "should be nil if artifacts browser is unsupported" do
- allow(build).to receive(:artifacts_metadata?).and_return(false)
- is_expected.to be_nil
- end
-
- it 'should not be nil if artifacts browser is supported' do
- allow(build).to receive(:artifacts_metadata?).and_return(true)
- is_expected.to_not be_nil
- end
- end
-
- describe :artifacts? do
+ describe '#artifacts?' do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
@@ -386,7 +358,7 @@ describe Ci::Build, models: true do
end
- describe :artifacts_metadata? do
+ describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? }
context 'artifacts metadata does not exist' do
it { is_expected.to be_falsy }
@@ -398,7 +370,7 @@ describe Ci::Build, models: true do
end
end
- describe :repo_url do
+ describe '#repo_url' do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
@@ -412,7 +384,7 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) }
end
- describe :depends_on_builds do
+ describe '#depends_on_builds' do
let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' }
let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' }
let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' }
@@ -444,7 +416,7 @@ describe Ci::Build, models: true do
created_at: created_at)
end
- describe :merge_request do
+ describe '#merge_request' do
context 'when a MR has a reference to the commit' do
before do
@merge_request = create_mr(build, commit, factory: :merge_request)
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index 4dc309a4255..412842337ba 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -32,50 +32,6 @@ describe Ci::Commit, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- describe :ordered do
- let(:project) { FactoryGirl.create :empty_project }
-
- it 'returns ordered list of commits' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
- expect(project.ci_commits.ordered).to eq([commit2, commit1])
- end
-
- it 'returns commits ordered by committed_at and id, with nulls last' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
- commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
- end
- end
-
- describe :last_build do
- subject { commit.last_build }
- before do
- @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
- @second = FactoryGirl.create :ci_build, commit: commit
- end
-
- it { is_expected.to be_a(Ci::Build) }
- it('returns with the most recently created build') { is_expected.to eq(@second) }
- end
-
- describe :retry do
- before do
- @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
- @second = FactoryGirl.create :ci_build, commit: commit
- end
-
- it "creates only a new build" do
- expect(commit.builds.count(:all)).to eq 2
- expect(commit.statuses.count(:all)).to eq 2
- commit.retry
- expect(commit.builds.count(:all)).to eq 3
- expect(commit.statuses.count(:all)).to eq 3
- end
- end
-
describe :valid_commit_sha do
context 'commit.sha can not start with 00000000' do
before do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index b854de1d3d5..6b18936edb1 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -152,7 +152,7 @@ describe Note, models: true do
end
end
- describe :grouped_awards do
+ describe '.grouped_awards' do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
@@ -169,6 +169,66 @@ describe Note, models: true do
end
end
+ describe '#active?' do
+ it 'is always true when the note has no associated diff' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(nil)
+
+ expect(note).to be_active
+ end
+
+ it 'is never true when the note has no noteable associated' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:noteable).and_return(nil)
+
+ expect(note).not_to be_active
+ end
+
+ it 'returns the memoized value if defined' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:noteable).and_return(double)
+
+ note.instance_variable_set(:@active, 'foo')
+ expect(note).not_to receive(:find_noteable_diff)
+
+ expect(note.active?).to eq 'foo'
+ end
+
+ context 'for a merge request noteable' do
+ it 'is false when noteable has no matching diff' do
+ merge = build_stubbed(:merge_request, :simple)
+ note = build(:note, noteable: merge)
+
+ allow(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:find_noteable_diff).and_return(nil)
+
+ expect(note).not_to be_active
+ end
+
+ it 'is true when noteable has a matching diff' do
+ merge = create(:merge_request, :simple)
+
+ # Generate a real line_code value so we know it will match. We use a
+ # random line from a random diff just for funsies.
+ diff = merge.diffs.to_a.sample
+ line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
+ code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+
+ # We're persisting in order to trigger the set_diff callback
+ note = create(:note, noteable: merge, line_code: code)
+
+ # Make sure we don't get a false positive from a guard clause
+ expect(note).to receive(:find_noteable_diff).and_call_original
+ expect(note).to be_active
+ end
+ end
+ end
+
describe "editable?" do
it "returns true" do
note = build(:note)
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
new file mode 100644
index 00000000000..2fa6715fcaf
--- /dev/null
+++ b/spec/models/project_group_link_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ProjectGroupLink do
+ describe "Associations" do
+ it { should belong_to(:group) }
+ it { should belong_to(:project) }
+ end
+
+ describe "Validation" do
+ let!(:project_group_link) { create(:project_group_link) }
+
+ it { should validate_presence_of(:project_id) }
+ it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
+ it { should validate_presence_of(:group_id) }
+ it { should validate_presence_of(:group_access) }
+ end
+end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 7b63da005f0..bacb17a8883 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -67,6 +67,50 @@ describe ProjectTeam, models: true do
end
end
+ describe :max_invited_level do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_user(master, Gitlab::Access::MASTER)
+ group.add_user(reporter, Gitlab::Access::REPORTER)
+ end
+
+ it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
+ end
+
+ describe :max_member_access do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_user(master, Gitlab::Access::MASTER)
+ group.add_user(reporter, Gitlab::Access::REPORTER)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+
+ it "does not have an access" do
+ project.namespace.update(share_with_group_lock: true)
+ expect(project.team.max_member_access(master.id)).to be_nil
+ expect(project.team.max_member_access(reporter.id)).to be_nil
+ end
+ end
+
describe "#human_max_access" do
it 'returns Master role' do
user = create(:user)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
new file mode 100644
index 00000000000..3722ddf5a33
--- /dev/null
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ describe 'GET /projects/:project_id/snippets/:id' do
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ it 'always exposes expires_at as nil' do
+ admin = create(:admin)
+ snippet = create(:project_snippet, author: admin)
+
+ get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+ expect(json_response).to have_key('expires_at')
+ expect(json_response['expires_at']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9f2365a4832..a6699cdc81c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -747,6 +747,42 @@ describe API::API, api: true do
end
end
+ describe "POST /projects/:id/share" do
+ let(:group) { create(:group) }
+
+ it "should share project with group" do
+ expect do
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ end.to change { ProjectGroupLink.count }.by(1)
+
+ expect(response.status).to eq 201
+ expect(json_response['group_id']).to eq group.id
+ expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
+ end
+
+ it "should return a 400 error when group id is not given" do
+ post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 400 error when access level is not given" do
+ post api("/projects/#{project.id}/share", user), group_id: group.id
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 400 error when sharing is disabled" do
+ project.namespace.update(share_with_group_lock: true)
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 409 error when wrong params passed" do
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
+ expect(response.status).to eq 409
+ expect(json_response['message']).to eq 'Group access is not included in the list'
+ end
+ end
+
describe 'GET /projects/search/:query' do
let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }