diff options
61 files changed, 1007 insertions, 147 deletions
diff --git a/CHANGELOG b/CHANGELOG index e6cdc09b9c6..a784a690ebf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,19 +18,24 @@ v 8.13.0 (unreleased) - Keep refs for each deployment - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Add more tests for calendar contribution (ClemMakesApps) + - Update Gitlab Shell to fix some problems with moving projects between storages - Cache rendered markdown in the database, rather than Redis - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Simplify Mentionable concern instance methods - Fix permission for setting an issue's due date - API: Multi-file commit !6096 (mahcsig) + - Add support for setting the GitLab Runners Registration Token during initial database seeding - Revert "Label list shows all issues (opened or closed) with that label" + - Add support for setting the GitLab Runners Registration Token during initial database seeding - Expose expires_at field when sharing project on API - Fix VueJS template tags being rendered in code comments - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Add Issue Board API support (andrebsguedes) + - Mattermost slash command support - Allow the Koding integration to be configured through the API - Add new issue button to each list on Issues Board - Added soft wrap button to repository file/blob editor + - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) @@ -65,6 +70,7 @@ v 8.13.0 (unreleased) - Add organization field to user profile - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) - Fix deploy status responsiveness error !6633 + - Make searching for commits case insensitive - Fix resolved discussion display in side-by-side diff view !6575 - Optimize GitHub importing for speed and memory - API: expose pipeline data in builds API (!6502, Guilherme Salazar) @@ -79,6 +85,8 @@ v 8.13.0 (unreleased) - Retouch environments list and deployments list - Add Container Registry on/off status to Admin Area !6638 (the-undefined) - Grouped pipeline dropdown is a scrollable container + - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) + - Fix a typo in doc/api/labels.md v 8.12.5 (unreleased) @@ -115,6 +123,7 @@ v 8.12.2 - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv) - Fix resolve discussion buttons endpoint path - Refactor remnants of CoffeeScript destructured opts and super !6261 + - Prevent running GfmAutocomplete setup for each diff note !6569 v 8.12.1 - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST @@ -318,6 +327,7 @@ v 8.11.7 - Avoid conflict with admin labels when importing GitHub labels. !6158 - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234 - Allow the Rails cookie to be used for API authentication. + - Updating verbiage on git basics to be more intuitive v 8.11.6 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 @@ -478,6 +488,7 @@ v 8.11.0 - Add pipeline events hook - Bump gitlab_git to speedup DiffCollection iterations - Rewrite description of a blocked user in admin settings. (Elias Werberich) + - Clarify documentation for Runners API (Gennady Trafimenkov) - Make branches sortable without push permission !5462 (winniehell) - Check for Ci::Build artifacts at database level on pipeline partial - Convert image diff background image to CSS (ClemMakesApps) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 4a788a01dad..0f44168a4d5 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.6.3 +3.6.4 diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index d0786bf0053..845313b6b38 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -52,37 +52,27 @@ } } }, - setup: function(input) { + setup: _.debounce(function(input) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); // destroy previous instances this.destroyAtWho(); // set up instances this.setupAtWho(); - if (this.dataSource) { - if (!this.dataLoading && !this.cachedData) { - this.dataLoading = true; - setTimeout((function(_this) { - return function() { - var fetch; - fetch = _this.fetchData(_this.dataSource); - return fetch.done(function(data) { - _this.dataLoading = false; - return _this.loadData(data); - }); - }; - // We should wait until initializations are done - // and only trigger the last .setup since - // The previous .dataSource belongs to the previous issuable - // and the last one will have the **proper** .dataSource property - // TODO: Make this a singleton and turn off events when moving to another page - })(this), 1000); - } - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } + + if (this.dataSource && !this.dataLoading && !this.cachedData) { + this.dataLoading = true; + return this.fetchData(this.dataSource) + .done((data) => { + this.dataLoading = false; + this.loadData(data); + }); + }; + + if (this.cachedData != null) { + return this.loadData(this.cachedData); } - }, + }, 1000), setupAtWho: function() { // Emoji this.input.atwho({ diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3f19e920166..f17cf5566bd 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,7 +1,9 @@ .environments-container, .deployments-container { - width: 100%; - overflow: auto; + @media (max-width: $screen-sm-max) { + width: 100%; + overflow: auto; + } } .environments { @@ -38,6 +40,14 @@ color: $gl-dark-link-color; } + .close-env-link { + color: $table-text-gray; + + .close-env-icon { + font-size: 14px; + } + } + .deployment { .build-column { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index bc8693ae467..f2c2e06ad3e 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -177,6 +177,15 @@ .ci-coverage { float: right; } + + .close-env-container { + color: $gl-text-color; + float: right; + + a { + color: $gl-text-color; + } + } } .mr_source_commit, diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 020e7365e38..3c86c15a240 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -590,6 +590,10 @@ } } +.terminal-icon { + margin-left: 3px; +} + .terminal-container { .content-block { border-bottom: none; diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb deleted file mode 100644 index 5bb7d499cdc..00000000000 --- a/app/controllers/ci/application_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - class ApplicationController < ::ApplicationController - def self.railtie_helpers_paths - "app/helpers/ci" - end - end -end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 78012960252..3eb485de9db 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,5 +1,5 @@ module Ci - class LintsController < ApplicationController + class LintsController < ::ApplicationController before_action :authenticate_user! def show diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index aa894fde36b..ff297d6ff13 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,5 +1,5 @@ module Ci - class ProjectsController < Ci::ApplicationController + class ProjectsController < ::ApplicationController before_action :project before_action :no_cache, only: [:badge] before_action :authorize_read_project!, except: [:badge, :index] diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 58678f96879..1740057e0da 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,11 +2,17 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:edit, :update, :destroy] - before_action :environment, only: [:show, :edit, :update, :destroy] + before_action :authorize_update_environment!, only: [:edit, :update, :close, :destroy] + before_action :environment, only: [:show, :edit, :update, :close, :destroy] def index - @environments = project.environments + @scope = params[:scope] + @all_environments = project.environments + @environments = + case @scope + when 'closed' then @all_environments.closed + else @all_environments.opened + end end def show @@ -48,6 +54,17 @@ class Projects::EnvironmentsController < Projects::ApplicationController redirect_to namespace_project_environments_path(project.namespace, project) end + def close + close_action = @environment.close_action + if close_action + close_build = close_action.play(current_user) + redirect_to namespace_project_build_path(project.namespace, project, close_build) + else + @environment.close + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + end + end + private def environment_params diff --git a/app/controllers/slash_commands_controller.rb b/app/controllers/slash_commands_controller.rb new file mode 100644 index 00000000000..b100ff00c38 --- /dev/null +++ b/app/controllers/slash_commands_controller.rb @@ -0,0 +1,65 @@ +class SlashCommandsController < ApplicationController + respond_to :json + + skip_before_action :verify_authenticity_token + skip_before_action :authenticate_user! + before_action :find_project + + attr_reader :project + + def trigger + if service + render json: service.new(@project, user, params).execute + else + render json: unavailable + end + end + + private + + def unavailable + { + response_type: :ephemeral, + text: 'This slash command has not been registered yet.', + } + end + + def project_unavailable(path) + { + response_type: :ephemeral, + text: "The #{path} doesn't exist or you don't have access." + } + end + + def project_not_found(path) + { + response_type: :ephemeral, + text: "We were unable to find a project for: #{path}" + } + end + + def find_project + path = "#{params[:team_domain]}/#{params[:channel_name]}" + @project = Project.find_with_namespace(path) + + render json: project_not_found(path) unless can?(user, :read_project, @project) + end + + def user + @user ||= User.find_by(username: params[:user_name]) + end + + def service + command = params[:command] + + if command == '/issue' && project.issues_enabled? && project.default_issues_tracker? + Mattermost::IssueService + elsif command == '/merge-request' && project.merge_requests_enabled? + Mattermost::MergeRequestService + elsif command == '/deploy' + Mattermost::DeployService + else + nil + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5dbf66173de..4f8a505da0d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -73,7 +73,7 @@ module Ci end end - state_machine :status do + state_machine :status, use_transactions: false do after_transition pending: :running do |build| build.execute_hooks end @@ -110,6 +110,14 @@ module Ci project.builds_enabled? && commands.present? && manual? && skipped? end + def close_environment? + options.fetch(:environment, {}).fetch(:close, false) + end + + def closes_environment?(name) + environment == name && close_environment? + end + def play(current_user = nil) # Try to queue a current build if self.enqueue @@ -121,6 +129,16 @@ module Ci end end + def play_type + return nil unless playable? + + if close_environment? + :close + else + :play + end + end + def retryable? project.builds_enabled? && commands.present? && complete? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2cf9892edc5..291256548ac 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -249,6 +249,8 @@ module Ci def process! Ci::ProcessPipelineService.new(project, user).execute(self) + + update_status end def update_status diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 9fa8d17e74e..c541085b7bd 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,6 +1,7 @@ class CommitStatus < ActiveRecord::Base include HasStatus include Importable + include AfterCommitQueue self.table_name = 'ci_builds' @@ -43,7 +44,7 @@ class CommitStatus < ActiveRecord::Base scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } - state_machine :status do + state_machine :status, use_transactions: false do event :enqueue do transition [:created, :skipped] => :pending end @@ -85,25 +86,34 @@ class CommitStatus < ActiveRecord::Base end after_transition do |commit_status, transition| - commit_status.pipeline.try do |pipeline| - break if transition.loopback? - - if commit_status.complete? - ProcessPipelineWorker.perform_async(pipeline.id) + return if transition.loopback? + + commit_status.run_after_commit do + pipeline.try do |pipeline| + if complete? + ProcessPipelineWorker.perform_async(pipeline.id) + else + UpdatePipelineWorker.perform_async(pipeline.id) + end end - - UpdatePipelineWorker.perform_async(pipeline.id) end - - true end after_transition [:created, :pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) + commit_status.run_after_commit do + # TODO, temporary fix for race condition + UpdatePipelineWorker.new.perform(pipeline.id) + + MergeRequests::MergeWhenBuildSucceedsService + .new(pipeline.project, nil).trigger(self) + end end after_transition any => :failed do |commit_status| - MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) + commit_status.run_after_commit do + MergeRequests::AddTodoWhenBuildFailsService + .new(pipeline.project, nil).execute(self) + end end end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 24c7b26d223..a4218a5d52a 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -35,6 +35,10 @@ module TokenAuthenticatable current_token.blank? ? write_new_token(token_field) : current_token end + define_method("set_#{token_field}") do |token| + write_attribute(token_field, token) if token + end + define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? read_attribute(token_field) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 82b27b78229..070c76339b1 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -77,6 +77,19 @@ class Deployment < ActiveRecord::Base take end + def close_action + return nil unless manual_actions + + @close_action ||= + manual_actions.find do |manual_action| + manual_action.try(:closes_environment?, deployable.environment) + end + end + + def closeable? + close_action.present? + end + private def ref_path diff --git a/app/models/environment.rb b/app/models/environment.rb index f0f3ee23223..6ec498ea2b7 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base allow_nil: true, addressable_url: true + delegate :closeable?, :close_action, to: :last_deployment, allow_nil: true + + scope :opened, -> { where(state: [:opened]) } + scope :closed, -> { where(state: [:closed]) } + + state_machine :state, initial: :opened do + event :close do + transition opened: :closed + end + + event :reopen do + transition closed: :opened + end + + state :opened + state :closed + end + def last_deployment deployments.last end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b7f2b2bbe61..b67049f0f55 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -61,15 +61,13 @@ class Namespace < ActiveRecord::Base def clean_path(path) path = path.dup # Get the email username by removing everything after an `@` sign. - path.gsub!(/@.*\z/, "") - # Usernames can't end in .git, so remove it. - path.gsub!(/\.git\z/, "") - # Remove dashes at the start of the username. - path.gsub!(/\A-+/, "") - # Remove periods at the end of the username. - path.gsub!(/\.+\z/, "") + path.gsub!(/@.*\z/, "") # Remove everything that's not in the list of allowed characters. - path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + # Remove trailing violations ('.atom', '.git', or '.') + path.gsub!(/(\.atom|\.git|\.)*\z/, "") + # Remove leading violations ('-') + path.gsub!(/\A\-+/, "") # Users with the great usernames of "." or ".." would end up with a blank username. # Work around that by setting their username to "blank", followed by a counter. diff --git a/app/models/project.rb b/app/models/project.rb index 88e4bd14860..43753bf5458 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -18,9 +18,11 @@ class Project < ActiveRecord::Base UNKNOWN_IMPORT_URL = 'http://unknown.git' - cache_markdown_field :description, pipeline: :description + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, + :merge_requests_enabled?, :issues_enabled?, to: :project_feature, + allow_nil: true - delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true + cache_markdown_field :description, pipeline: :description default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level @@ -1305,7 +1307,7 @@ class Project < ActiveRecord::Base environment_ids.where(ref: ref) end - environments.where(id: environment_ids).select do |environment| + environments.opened.where(id: environment_ids).select do |environment| environment.includes_commit?(commit) end end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 530f7d5a30e..e70c0c3bbf9 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -38,23 +38,21 @@ class ProjectFeature < ActiveRecord::Base end def builds_enabled? - return true unless builds_access_level - builds_access_level > DISABLED end def wiki_enabled? - return true unless wiki_access_level - wiki_access_level > DISABLED end def merge_requests_enabled? - return true unless merge_requests_access_level - merge_requests_access_level > DISABLED end + def issues_enabled? + issues_access_level > DISABLED + end + private def get_permission(user, level) diff --git a/app/models/repository.rb b/app/models/repository.rb index bf59b74495b..4da1933c189 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -111,8 +111,10 @@ class Repository def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) ref ||= root_ref - # Limited to 1000 commits for now, could be parameterized? - args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query}) + args = %W( + #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} + --max-count #{limit} --grep=#{query} --regexp-ignore-case + ) args = args.concat(%W(-- #{path})) if path.present? git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 36c93dddadb..d3dd30b2588 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -16,6 +16,8 @@ module Ci process_stage(index) end + @pipeline.update_status + # Return a flag if a when builds got enqueued new_builds.flatten.any? end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 799ad3e1bd0..c87542e57a2 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -4,6 +4,13 @@ class CreateDeploymentService < BaseService def execute(deployable = nil) environment = find_or_create_environment + if close? + environment.close + return + end + + environment.reopen + deployment = project.deployments.create( environment: environment, ref: params[:ref], @@ -14,7 +21,6 @@ class CreateDeploymentService < BaseService ) deployment.update_merge_request_metrics! - deployment end @@ -44,6 +50,10 @@ class CreateDeploymentService < BaseService options[:url] end + def close? + options[:close] + end + def options params[:options] || {} end diff --git a/app/services/mattermost/base_service.rb b/app/services/mattermost/base_service.rb new file mode 100644 index 00000000000..d96d4eff13a --- /dev/null +++ b/app/services/mattermost/base_service.rb @@ -0,0 +1,52 @@ +module Mattermost + class BaseService < ::BaseService + def execute + # Implement in child + raise NotImplementedError + end + + private + + def resource_id + match = params[:text].to_s.match(/\A(\$|#|!)?(?<id>\d+)\z/) + + match ? match[:id] : nil + end + + + def generate_response(resource) + return respond_404 if resource.nil? + return single_resource(resource) unless resource.respond_to?(:count) + return no_search_results if resource.empty? + + if resource.count == 1 + single_resource(resource.first) + else + multiple_resources(resource) + end + end + + def respond_404 + { + response_type: :ephemeral, + text: "404 not found! GitLab couldn't find what your were looking for! :boom:", + } + end + + def no_search_results + { + response_type: :ephemeral, + text: "### No search results for \"#{params[:text]}\". :disappointed:" + } + end + + def multiple_resources(resources) + list = resources.map { |r| "#{r.to_reference} #{r.title}" }.join('\n\n') + + { + response_type: :ephemeral, + text: "### Search results for \"#{params[:text]}\"\n\n#{list}" + } + end + end +end diff --git a/app/services/mattermost/deploy_service.rb b/app/services/mattermost/deploy_service.rb new file mode 100644 index 00000000000..1ec2fb1c30b --- /dev/null +++ b/app/services/mattermost/deploy_service.rb @@ -0,0 +1,48 @@ +module Mattermost + class DeployService < BaseService + def execute + environment_name, action_name = parse_command + return respond_404 unless environment_name + + environment = project.environments.find_by(name: environment_name) + + return respond_404 unless can?(current_user, :read_environment, environment) + + deployment = environment.last_deployment + return respond_404 unless can?(current_user, :create_deployment, deployment) && deployment.deployable + + manual_action = deployment.manual_actions.find { |action| action.name = action_name } + + if manual_action + new_build = manual_action.play(current_user) + generate_response(new_build) + else + { + response_type: :ephemeral, + text: "No action '#{action_name}' defined for #{environment_name}." + } + end + end + + private + + def single_resource(build) + { + response_type: :in_channel, + text: "Action '#{action_name}' started on '#{environment_name}' you can [follow the progress](#{link(new_build)})." + } + end + + def link(build) + Gitlab::Routing. + url_helpers. + namespace_project_build_url(project.namespace, project, build) + end + + def parse_command + matches = params[:text].match(/\A(?<name>\w+) to (?<action>\w+)/) + + matches ? [matches[:name], matches[:action]] : nil + end + end +end diff --git a/app/services/mattermost/issue_service.rb b/app/services/mattermost/issue_service.rb new file mode 100644 index 00000000000..e1429c23438 --- /dev/null +++ b/app/services/mattermost/issue_service.rb @@ -0,0 +1,84 @@ +module Mattermost + class IssueService < BaseService + SUBCOMMANDS = ['create', 'search'].freeze + + def execute + resource = if resource_id + find_resource + elsif subcommand + send(subcommand) + else + nil + end + + generate_response(resource) + end + + private + + def find_resource + return nil unless can?(current_user, :read_issue, project) + + collection.find_by(iid: resource_id) + end + + def create + return nil unless can?(current_user, :create_issue, project) + + Issues::CreateService.new(project, current_user, issue_params).execute + end + + def search + return nil unless can?(current_user, :read_issue, project) + + query = params[:text].gsub(/\Asearch /, '') + collection.search(query).limit(5) + end + + def issue_create_error(errors) + { + response_type: :ephemeral, + text: "An error occured creating your issue: #{errors}" #TODO; this displays an array + } + end + + def single_resource(issue) + return issue_create_error(issue) if issue.errors.any? + + message = "### [#{issue.to_reference} #{issue.title}](#{link(issue)})" + message << "\n\n#{issue.description}" if issue.description + + { + response_type: :in_channel, + text: message + } + end + + def collection + project.issues + end + + def link(issue) + Gitlab::Routing. + url_helpers. + namespace_project_issue_url(project.namespace, project, issue) + end + + def issue_params + match = params[:text].match(/\Acreate (?<title>.+$)/) + + return issue_create_error("No title given") unless match + + { + title: match[:title], + description: params[:text].gsub(/\Acreate .+$\s*/, '') + } + end + + def subcommand + match = params[:text].match(/\A(?<subcommand>(#{SUBCOMMANDS.join('|')}))/) + + match ? match[:subcommand] : nil + end + end +end diff --git a/app/services/mattermost/merge_request_service.rb b/app/services/mattermost/merge_request_service.rb new file mode 100644 index 00000000000..f2239fee4c2 --- /dev/null +++ b/app/services/mattermost/merge_request_service.rb @@ -0,0 +1,15 @@ +module Mattermost + class MergeRequestService < BaseService + private + + def collection + project.merge_requests + end + + def link(merge_request) + Gitlab::Routing. + url_helpers. + namespace_project_merge_request_url(project.namespace, project, merge_request) + end + end +end diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f5344091cae..966633f1f89 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -128,7 +128,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{class: ('active' if build == @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('right-arrow') + = icon('arrow-right') = ci_icon_for_status(build.status) %span - if build.name diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 9f249c43673..716eb8d2c12 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,15 +1,17 @@ -- if can?(current_user, :create_deployment, deployment) && deployment.deployable - .pull-right - +.pull-right + - if local_assigns.fetch(:show_external_url, false) - external_url = deployment.environment.external_url - if external_url = link_to external_url, target: '_blank', class: 'btn external-url' do = icon('external-link') + - if can?(current_user, :update_deployment, deployment) && deployment.deployable + %a.btn.btn-default.btn-terminal{href: "deployments/#{deployment.id}/terminal"} + = custom_icon('icon_terminal') + + - if can?(current_user, :create_deployment, deployment) && deployment.deployable && local_assigns.fetch(:show_actions, false) - actions = deployment.manual_actions - if actions.present? - %a.btn.btn-default.btn-terminal{href: 'deployments/1/terminal'} - = custom_icon('icon_terminal') .inline .dropdown %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} @@ -22,9 +24,14 @@ = custom_icon('icon_play') %span= action.name.humanize - - if local_assigns.fetch(:allow_rollback, false) - = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do - - if deployment.last? - Re-deploy - - else - Rollback + - if can?(current_user, :update_deployment, deployment) && local_assigns.fetch(:allow_close, false) && deployment.closeable? + .inline + = link_to close_namespace_project_environment_path(@project.namespace.becomes(Namespace), @project, deployment.environment), method: :post, class: 'btn close-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to close this environment?' } do + = icon('stop', class: 'close-env-icon') + + - if can?(current_user, :create_deployment, deployment) && deployment.deployable && local_assigns.fetch(:allow_rollback, false) + = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn' do + - if deployment.last? + Re-deploy + - else + Rollback diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index ca0005abd0c..05dabe11871 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,4 +17,4 @@ #{time_ago_with_tooltip(deployment.created_at)} %td.hidden-xs - = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true + = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true, show_actions: true diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 251694e897c..760bca572c9 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -28,4 +28,6 @@ #{time_ago_with_tooltip(last_deployment.created_at)} %td.hidden-xs - = render 'projects/deployments/actions', deployment: last_deployment + = render 'projects/deployments/actions', deployment: last_deployment, + allow_close: environment.opened?, allow_rollback: environment.closed?, + show_external_url: environment.opened?, show_actions: environment.opened? diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index ab801409722..c83494cebd8 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,14 +3,27 @@ = render "projects/pipelines/head" %div{ class: container_class } - - if can?(current_user, :create_environment, @project) && !@environments.blank? - .top-area - .nav-controls + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_environments_path(@project) do + Available + %span.badge.js-avaibale-environments-count + = number_with_delimiter(@all_environments.opened.count) + + %li{class: ('active' if @scope == 'closed')} + = link_to project_environments_path(@project, scope: :closed) do + Stopped + %span.badge.js-stopped-environments-count + = number_with_delimiter(@all_environments.closed.count) + + .nav-controls + - if can?(current_user, :create_environment, @project) && !@all_environments.blank? = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment .environments-container - - if @environments.blank? + - if @all_environments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title You don't have any environments right now. diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7a8d196cf4e..01765f8008d 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -4,13 +4,18 @@ %div{ class: container_class } .top-area - .col-md-9 - %h3.page-title= @environment.name.capitalize - .col-md-3 - .nav-controls - - if can?(current_user, :update_environment, @environment) - = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete + .row + .col-sm-6 + %h3.page-title= @environment.name.capitalize + .col-sm-6 + .nav-controls + - if @environment.external_url + = link_to @environment.external_url, target: '_blank', class: 'btn external-url' do + = icon('external-link') + - if can?(current_user, :update_environment, @environment) + = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' + - if @environment.opened? + = link_to 'Close', close_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to close this environment?' }, class: 'btn btn-danger', method: :post .deployments-container - if @deployments.blank? diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 5b7f83c344f..bc68724ccf0 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -58,3 +58,7 @@ = link_to external_url, target: '_blank' do %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')} = icon('external-link', right: true) + %span.close-env-container + - if environment.closeable? + = link_to [:play, @project.namespace.becomes(Namespace), @project, environment.close_action], method: :post, class: 'close-evn-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to close this environment?' } do + = icon('stop-circle-o', text: 'Stop environment') diff --git a/config/routes.rb b/config/routes.rb index bf7c5b76128..243fd57bf5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,10 @@ Rails.application.routes.draw do # Notification settings resources :notification_settings, only: [:create, :update] + namespace :slash_commands do + post :trigger + end + draw :import draw :uploads draw :explore diff --git a/config/routes/project.rb b/config/routes/project.rb index bf991c97034..29aec7d260d 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -316,7 +316,17 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: end end - resources :environments + resources :environments do + member do + post :close + end + end + + resources :deployments, only: [] do + member do + get :terminal + end + end resources :deployments, only: [] do member do diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 803cbca584d..d3fabe111a1 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -17,6 +17,7 @@ class Gitlab::Seeder::Pipelines { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled }, { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success }, + { name: 'close staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped, options: { environment: { close: true } } }, { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped }, { name: 'slack', stage: 'notify', when: 'manual', status: :created }, ] diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb new file mode 100644 index 00000000000..5522f31629a --- /dev/null +++ b/db/fixtures/production/010_settings.rb @@ -0,0 +1,16 @@ +if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + + if settings.save + puts "Saved Runner Registration Token".color(:green) + else + puts "Could not save Runner Registration Token".color(:red) + puts + settings.errors.full_messages.map do |message| + puts "--> #{message}".color(:red) + end + puts + exit 1 + end +end diff --git a/db/migrate/20161006104309_add_state_to_environment.rb b/db/migrate/20161006104309_add_state_to_environment.rb new file mode 100644 index 00000000000..83dff07069f --- /dev/null +++ b/db/migrate/20161006104309_add_state_to_environment.rb @@ -0,0 +1,15 @@ +class AddStateToEnvironment < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:environments, :state, :string, default: :opened) + end + + def down + remove_column(:environments, :state) + end +end diff --git a/db/schema.rb b/db/schema.rb index 56da70b3c02..97ed7335c4b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160926145521) do +ActiveRecord::Schema.define(version: 20161006104309) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -399,11 +399,12 @@ ActiveRecord::Schema.define(version: 20160926145521) do create_table "environments", force: :cascade do |t| t.integer "project_id" - t.string "name", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" t.string "external_url" t.string "environment_type" + t.string "state", default: "opened", null: false end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index b4a953d1ccc..76029b30dd8 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,17 +13,18 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- -`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation -`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) -`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` -`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` -`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab -`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer -`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation +`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) +`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` +`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` +`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab +`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab +`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer +`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for GitLab Runners ## Complete database variables diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md index ecec53fde03..16028d1f124 100644 --- a/doc/api/ci/runners.md +++ b/doc/api/ci/runners.md @@ -12,7 +12,9 @@ communication channel. For the consumer API see the This API uses two types of authentication: 1. Unique Runner's token, which is the token assigned to the Runner after it - has been registered. + has been registered. This token can be found on the Runner's edit page (go to + **Project > Runners**, select one of the Runners listed under **Runners activated for + this project**). 2. Using Runners' registration token. This is a token that can be found in project's settings. @@ -48,7 +50,7 @@ DELETE /ci/api/v1/runners/delete | Attribute | Type | Required | Description | | --------- | ------- | --------- | ----------- | -| `token` | string | yes | Runner's registration token | +| `token` | string | yes | Unique Runner's token | Example request: diff --git a/doc/api/labels.md b/doc/api/labels.md index 3653ccf304a..656232cc940 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -148,7 +148,7 @@ PUT /projects/:id/labels | --------------- | ------- | --------------------------------- | ------------------------------- | | `id` | integer | yes | The ID of the project | | `name` | string | yes | The name of the existing label | -| `new_name` | string | yes if `color` if not provided | The new name of the label | +| `new_name` | string | yes if `color` is not provided | The new name of the label | | `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign | | `description` | string | no | The new description of the label | diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index b61f436c1a4..42cd8bb3e48 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -1,11 +1,10 @@ # Start using Git on the command line -If you want to start using a Git and GitLab, make sure that you have created an -account on GitLab. +If you want to start using Git and GitLab, make sure that you have created and/or signed into an account on GitLab. ## Open a shell -Depending on your operating system, find the shell of your preference. Here are some suggestions. +Depending on your operating system, you will need to use a shell of your preference. Here are some suggestions: - [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on Mac OSX @@ -22,19 +21,19 @@ Type the following command and then press enter: git --version ``` -You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). +You should receive a message that will tell you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window. -After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed. +After you are finished installing, open a new shell and type "git --version" again to verify that it was correctly installed. ## Add your Git username and set your email -It is important because every Git commit that you create will use this information. +It is important to configure your Git username and email address as every Git commit will use this information to identify you as the author. On your shell, type the following command to add your username: ``` -git config --global user.name ADD YOUR USERNAME +git config --global user.name "YOUR_USERNAME" ``` Then verify that you have the correct username: @@ -44,7 +43,7 @@ git config --global user.name To set your email address, type the following command: ``` -git config --global user.email ADD YOUR EMAIL +git config --global user.email "your_email_address@example.com" ``` To verify that you entered your email correctly, type: @@ -52,7 +51,7 @@ To verify that you entered your email correctly, type: git config --global user.email ``` -You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project. +You'll need to do this only once as you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project. ## Check your information @@ -76,7 +75,7 @@ git pull REMOTE NAME-OF-BRANCH -u (REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch) ### Create a branch -Spaces won't be recognized, so you need to use a hyphen or underscore. +Spaces won't be recognized, so you will need to use a hyphen or underscore. ``` git checkout -b NAME-OF-BRANCH ``` @@ -127,4 +126,3 @@ You need to be in the master branch. git checkout master git merge NAME-OF-BRANCH ``` - diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb index 7b1533d0d32..9fac4831752 100644 --- a/lib/expand_variables.rb +++ b/lib/expand_variables.rb @@ -4,7 +4,7 @@ module ExpandVariables # Convert hash array to variables if variables.is_a?(Array) variables = variables.reduce({}) do |hash, variable| - hash[variable[:key]] = variable[:value] + hash[variable[:key].to_s] = variable[:value] hash end end diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb index d388ab6b879..da016becc96 100644 --- a/lib/gitlab/ci/config/node/environment.rb +++ b/lib/gitlab/ci/config/node/environment.rb @@ -8,7 +8,7 @@ module Gitlab class Environment < Entry include Validatable - ALLOWED_KEYS = %i[name url] + ALLOWED_KEYS = %i[name url close] validations do validate do @@ -35,6 +35,8 @@ module Gitlab length: { maximum: 255 }, addressable_url: true, allow_nil: true + + validates :close, boolean: true, allow_nil: true end end @@ -54,6 +56,10 @@ module Gitlab value[:url] end + def close + value[:close] + end + def value case @config when String then { name: @config } diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 776bbcbb5d0..0d30e1bb92e 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -2,7 +2,7 @@ module Gitlab module Regex extend self - NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze + NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze def namespace_regex @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze @@ -10,7 +10,7 @@ module Gitlab def namespace_regex_message "can contain only letters, digits, '_', '-' and '.'. " \ - "Cannot start with '-' or end in '.'." \ + "Cannot start with '-' or end in '.', '.git' or '.atom'." \ end def namespace_name_regex diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 5d33f98e89e..594439a5d4b 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -111,7 +111,7 @@ module Gitlab def write_secret bytes = SecureRandom.random_bytes(SECRET_LENGTH) File.open(secret_path, 'w:BINARY', 0600) do |f| - f.chmod(0600) + f.chmod(0600) # If the file already existed, the '0600' passed to 'open' above was a no-op. f.write(Base64.strict_encode64(bytes)) end end diff --git a/spec/controllers/slash_commands_controller_spec.rb b/spec/controllers/slash_commands_controller_spec.rb new file mode 100644 index 00000000000..6095130f679 --- /dev/null +++ b/spec/controllers/slash_commands_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SlashCommandsController, type: :controller do + +end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb new file mode 100644 index 00000000000..a7c5283df94 --- /dev/null +++ b/spec/db/production/settings.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789') + end + + it 'writes the token to the database' do + load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) + expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') + end + end +end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 68ea4eeae31..35f5706f920 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -18,11 +18,26 @@ feature 'Environments', feature: true do before do visit namespace_project_environments_path(project.namespace, project) end + + context 'shows two tabs' do + scenario 'does show Available tab with link' do + expect(page).to have_link('Available') + end + + scenario 'does show Stopped tab with link' do + expect(page).to have_link('Stopped') + end + end context 'without environments' do scenario 'does show no environments' do expect(page).to have_content('You don\'t have any environments right now.') end + + scenario 'does show 0 as counter for environments in both tabs' do + expect(page.find('.js-avaibale-environments-count').text).to eq('0') + expect(page.find('.js-stopped-environments-count').text).to eq('0') + end end context 'with environments' do @@ -31,6 +46,14 @@ feature 'Environments', feature: true do scenario 'does show environment name' do expect(page).to have_link(environment.name) end + + scenario 'does show number of opened environments in Availabe tab' do + expect(page.find('.js-avaibale-environments-count').text).to eq('1') + end + + scenario 'does show number of closed environments in Stopped tab' do + expect(page.find('.js-stopped-environments-count').text).to eq('0') + end context 'without deployments' do scenario 'does show no deployments' do @@ -79,6 +102,16 @@ feature 'Environments', feature: true do expect(page).to have_link(nil, href: environment.external_url) end end + + scenario 'does show close button' do + # TODO: Add test to verify if close button is visible + # This needs to be true: if local_assigns.fetch(:allow_close, false) && deployment.closeable? + end + + scenario 'does allow to close environment' do + # TODO: Add test to verify if close environment works + # This needs to be true: if local_assigns.fetch(:allow_close, false) && deployment.closeable? + end end end end @@ -150,6 +183,16 @@ feature 'Environments', feature: true do expect(page).to have_link(nil, href: environment.external_url) end end + + scenario 'does show close button' do + # TODO: Add test to verify if close button is visible + # This needs to be true: if local_assigns.fetch(:allow_close, false) && deployment.closeable? + end + + scenario 'does allow to close environment' do + # TODO: Add test to verify if close environment works + # This needs to be true: if local_assigns.fetch(:allow_close, false) && deployment.closeable? + end end end end @@ -203,21 +246,34 @@ feature 'Environments', feature: true do before do visit namespace_project_environment_path(project.namespace, project, environment) end - + context 'when logged as master' do given(:role) { :master } - scenario 'does delete environment' do - click_link 'Destroy' - expect(page).not_to have_link(environment.name) + scenario 'does not have a Close link' do + expect(page).not_to have_link('Close') + end + + context 'when environment is opened and can be closed' do + let(:project) { create(:project) } + let(:environment) { create(:environment, project: project) } + + let!(:deployment) do + create(:deployment, environment: environment, sha: project.commit('master').id) + end + + scenario 'does have a Close link' do + # TODO: Add missing validation. In order to have Close link + # this must be true: last_deployment.try(:close_action) + end end end context 'when logged as developer' do given(:role) { :developer } - scenario 'does not have a Destroy link' do - expect(page).not_to have_link('Destroy') + scenario 'does not have a Close link' do + expect(page).not_to have_link('Close') end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 2d8b59472e8..c54ec2563ad 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -5,6 +5,12 @@ feature 'Group', feature: true do login_as(:admin) end + matcher :have_namespace_error_message do + match do |page| + page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.") + end + end + describe 'creating a group with space in group path' do it 'renders new group form with validation errors' do visit new_group_path @@ -13,7 +19,31 @@ feature 'Group', feature: true do click_button 'Create group' expect(current_path).to eq(groups_path) - expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.") + expect(page).to have_namespace_error_message + end + end + + describe 'creating a group with .atom at end of group path' do + it 'renders new group form with validation errors' do + visit new_group_path + fill_in 'Group path', with: 'atom_group.atom' + + click_button 'Create group' + + expect(current_path).to eq(groups_path) + expect(page).to have_namespace_error_message + end + end + + describe 'creating a group with .git at end of group path' do + it 'renders new group form with validation errors' do + visit new_group_path + fill_in 'Group path', with: 'git_group.git' + + click_button 'Create group' + + expect(current_path).to eq(groups_path) + expect(page).to have_namespace_error_message end end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 60bc07bd1a0..2c1a45af596 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -79,6 +79,15 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end end + context 'Has Environment' do + let(:environment) { create(:environment, project: project) } + + it 'does show link to close the environment' do + # TODO add test to verify if the button is visible when this condition + # is met: if environment.closeable? + end + end + def visit_merge_request(merge_request) visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) end diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb index df453223da7..d46cc4355ea 100644 --- a/spec/lib/gitlab/ci/config/node/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb @@ -100,6 +100,34 @@ describe Gitlab::Ci::Config::Node::Environment do end end + context 'when close is used' do + context 'when close is a boolean' do + let(:config) do + { name: 'review/$CI_BUILD_REF_NAME', + close: true } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when close is a string' do + let(:config) do + { name: 'review/$CI_BUILD_REF_NAME', + close: 'string' } + end + + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end + end + context 'when configuration is invalid' do context 'when configuration is an array' do let(:config) { ['env'] } diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index eb64f3d0c83..4b0bfa43abf 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } + it { is_expected.to respond_to("set_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } end end @@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end end + describe 'setting new token' do + subject { described_class.new.send("set_#{token_field}", '0123456789') } + + it { is_expected.to eq '0123456789' } + end + describe 'multiple token fields' do before do described_class.send(:add_authentication_token_field, :yet_another_token) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 544920d1824..431b3e4435f 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -114,6 +114,7 @@ describe Namespace, models: true do it "cleans the path and makes sure it's available" do expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") + expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name") end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 98c64c079b9..4641f297465 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -97,12 +97,20 @@ describe Repository, models: true do end describe '#find_commits_by_message' do - subject { repository.find_commits_by_message('submodule').map{ |k| k.id } } + it 'returns commits with messages containing a given string' do + commit_ids = repository.find_commits_by_message('submodule').map(&:id) - it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } - it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } - it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') } - it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + expect(commit_ids).to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + expect(commit_ids).to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') + expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') + end + + it 'is case insensitive' do + commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id) + + expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + end end describe '#blob_at' do diff --git a/spec/services/mattermost/deploy_service_spec.rb b/spec/services/mattermost/deploy_service_spec.rb new file mode 100644 index 00000000000..5e6f44518a8 --- /dev/null +++ b/spec/services/mattermost/deploy_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Mattermost::DeployService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + let(:service) { described_class.new(project, user, params) } + + shared_examples 'a 404 response' do + it 'responds with a 404 message' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + + describe '#execute' do + let(:params) { { text: 'envname to action' } } + subject { service.execute } + + context 'when the environment can not be found' do + it_behaves_like 'a 404 response' + end + + context 'the environment exists' do + end + end +end diff --git a/spec/services/mattermost/issue_service_spec.rb b/spec/services/mattermost/issue_service_spec.rb new file mode 100644 index 00000000000..b45971bdf9b --- /dev/null +++ b/spec/services/mattermost/issue_service_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +describe Mattermost::IssueService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + let(:service) { described_class.new(project, user, params) } + + shared_examples 'a 404 response' do + it 'responds with a 404 message' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + + describe '#execute' do + subject { service.execute } + + context 'Looking up on iid' do + let(:params) { { text: 1 } } + + context 'when the user can not read issues' do + it_behaves_like 'a 404 response' + end + + context 'when the user has access' do + context 'when the resource exists' do + let(:issue) { create(:issue, project: project) } + let(:params) { { text: issue.iid } } + + before do + project.team << [user, :master] + end + + it 'returns the resource' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to start_with "### [#{issue.to_reference} " + end + end + + context 'when the resource does not exists' do + it_behaves_like 'a 404 response' + end + end + end + + context 'searching for issues' do + let!(:issue) { create(:issue, project: project, title: 'Bird is the word') } + let!(:issue1) { create(:issue, project: project, title: 'Everybody heard about the bird') } + let(:params) { { text: 'search bird' } } + + context 'when the user has no access' do + it_behaves_like 'a 404 response' + end + + context 'when the user has acces' do + before do + project.team << [user, :master] + end + + context 'when there are results' do + it 'returns the resource' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with "### Search results for" + end + end + + context 'when there is only one result' do + let(:params) { { text: 'search about the bird' } } + + it 'returns the resource' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to start_with "### [#{issue1.to_reference} " + end + end + end + end + + context 'creating an issue' do + let(:params) { { text: "create The Trashmen\nBird is the word" } } + + context 'when the user has no access' do + it_behaves_like 'a 404 response' + end + + context 'when the user has acces' do + before do + project.team << [user, :master] + end + + context 'it creates an issue' do + it 'returns the resource' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match /The Trashmen/ + end + end + end + end + end +end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 520e906b21f..9a29e400654 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -110,9 +110,21 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } - let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } - let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) } + let(:sha) { project.commit(ref).id } + + let(:pipeline) do + create(:ci_empty_pipeline, ref: ref, sha: sha, project: project) + end + + let!(:build) do + create(:ci_build, :created, pipeline: pipeline, ref: ref, + name: 'build', stage: 'build') + end + + let!(:test) do + create(:ci_build, :created, pipeline: pipeline, ref: ref, + name: 'test', stage: 'test') + end before do # This behavior of MergeRequest: we instantiate a new object @@ -121,14 +133,16 @@ describe MergeRequests::MergeWhenBuildSucceedsService do end end - it "doesn't merge if some stages failed" do + it "doesn't merge if any of stages failed" do expect(MergeWorker).not_to receive(:perform_async) + build.success test.drop end - it 'merge when all stages succeeded' do + it 'merges when all stages succeeded' do expect(MergeWorker).to receive(:perform_async) + build.success test.success end diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml new file mode 100644 index 00000000000..75924516fb2 --- /dev/null +++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml @@ -0,0 +1,99 @@ +image: ayufan/openshift-cli + +stages: + - build + - test + - staging + - production + +variables: + # Configure these variables in Secure Variables: + OPENSHIFT_SERVER: https://origin.gitlap.com:8443 + OPENSHIFT_DOMAIN: apps.origin.gitlap.com + # OPENSHIFT_TOKEN: my.openshift.token + +before_script: + - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify + - oc project "$CI_PROJECT_NAME" + +build: + stage: build + before_script: [] + script: + - echo build + +test1: + stage: test + before_script: [] + script: + - echo run tests + +test2: + stage: test + before_script: [] + script: + - echo run tests + +.deploy: &deploy + image: ayufan/openshift-cli + script: + - oc get services $APP || oc new-app . --name=$APP --strategy=docker + - oc get routes $APP || oc expose service $APP --hostname=$APP_HOST + - oc start-build $APP --from-dir=. --wait + +production: + <<: *deploy + stage: production + variables: + APP: production + APP_HOST: production.$OPENSHIFT_DOMAIN + when: manual + environment: + name: production + url: http://production.$OPENSHIFT_DOMAIN + only: + - master + +staging: + <<: *deploy + stage: staging + variables: + APP: staging + APP_HOST: staging.$OPENSHIFT_DOMAIN + environment: + name: staging + url: http://staging.$OPENSHIFT_DOMAIN + only: + - master + +review: + <<: *deploy + stage: staging + variables: + APP: review-$CI_BUILD_REF_NAME + APP_HOST: $CI_BUILD_REF_NAME.review.$OPENSHIFT_DOMAIN + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_BUILD_REF_NAME.review.$OPENSHIFT_DOMAIN + only: + - branches + except: + - master + +close review: + <<: *deploy + stage: staging + script: + - oc delete all -l "app=$APP" + when: manual + variables: + APP: review-$CI_BUILD_REF_NAME + environment: + name: review/$CI_BUILD_REF_NAME + close: true + only: + - branches + except: + - master + + |