diff options
Diffstat (limited to 'app')
36 files changed, 458 insertions, 198 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f20162c48e9..970e83c0ecb 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -13,7 +13,7 @@ import GroupLabelSubscription from './group_label_subscription'; /* global LineHighlighter */ import BuildArtifacts from './build_artifacts'; import CILintEditor from './ci_lint_editor'; -/* global GroupsSelect */ +import groupsSelect from './groups_select'; /* global Search */ /* global Admin */ /* global NamespaceSelects */ @@ -414,7 +414,7 @@ import Diff from './diff'; break; case 'projects:project_members:index': memberExpirationDate('.js-access-expiration-date-groups'); - new GroupsSelect(); + groupsSelect(); memberExpirationDate(); new Members(); new UsersSelect(); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 90ca70289ab..a69a0bde17b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,121 +1,86 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, - camelcase, one-var-declaration-per-line, quotes, object-shorthand, - prefer-arrow-callback, comma-dangle, consistent-return, yoda, - prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, - promise/catch-or-return */ import Api from './api'; import { normalizeCRLFHeaders } from './lib/utils/common_utils'; -var slice = [].slice; +export default function groupsSelect() { + // Needs to be accessible in rspec + window.GROUP_SELECT_PER_PAGE = 20; + $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { + const $select = $(this); + const allAvailable = $select.data('all-available'); + const skipGroups = $select.data('skip-groups') || []; + $select.select2({ + placeholder: 'Search for a group', + multiple: $select.hasClass('multiselect'), + minimumInputLength: 0, + ajax: { + url: Api.buildUrl(Api.groupsPath), + dataType: 'json', + quietMillis: 250, + transport(params) { + return $.ajax(params) + .then((data, status, xhr) => { + const results = data || []; -window.GroupsSelect = (function() { - function GroupsSelect() { - $('.ajax-groups-select').each((function(_this) { - const self = _this; - - return function(i, select) { - var all_available, skip_groups; - const $select = $(select); - all_available = $select.data('all-available'); - skip_groups = $select.data('skip-groups') || []; - - $select.select2({ - placeholder: "Search for a group", - multiple: $select.hasClass('multiselect'), - minimumInputLength: 0, - ajax: { - url: Api.buildUrl(Api.groupsPath), - dataType: 'json', - quietMillis: 250, - transport: function (params) { - $.ajax(params).then((data, status, xhr) => { - const results = data || []; - - const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); - const currentPage = parseInt(headers['X-PAGE'], 10) || 0; - const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; - const more = currentPage < totalPages; - - return { - results, - pagination: { - more, - }, - }; - }).then(params.success).fail(params.error); - }, - data: function (search, page) { - return { - search, - page, - per_page: GroupsSelect.PER_PAGE, - all_available, - }; - }, - results: function (data, page) { - if (data.length) return { results: [] }; - - const groups = data.length ? data : data.results || []; - const more = data.pagination ? data.pagination.more : false; - const results = groups.filter(group => skip_groups.indexOf(group.id) === -1); + const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; return { results, - page, - more, + pagination: { + more, + }, }; - }, - }, - initSelection: function(element, callback) { - var id; - id = $(element).val(); - if (id !== "") { - return Api.group(id, callback); - } - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return self.formatResult.apply(self, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return self.formatSelection.apply(self, args); - }, - dropdownCssClass: "ajax-groups-dropdown select2-infinite", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; - } - }); - - self.dropdown = document.querySelector('.select2-infinite .select2-results'); - - $select.on('select2-loaded', self.forceOverflow.bind(self)); - }; - })(this)); - } - - GroupsSelect.prototype.formatResult = function(group) { - var avatar; - if (group.avatar_url) { - avatar = group.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>"; - }; - - GroupsSelect.prototype.formatSelection = function(group) { - return group.full_name; - }; + }) + .then(params.success) + .fail(params.error); + }, + data(search, page) { + return { + search, + page, + per_page: window.GROUP_SELECT_PER_PAGE, + all_available: allAvailable, + }; + }, + results(data, page) { + if (data.length) return { results: [] }; - GroupsSelect.prototype.forceOverflow = function (e) { - this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; - }; + const groups = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); - GroupsSelect.PER_PAGE = 20; + return { + results, + page, + more, + }; + }, + }, + // eslint-disable-next-line consistent-return + initSelection(element, callback) { + const id = $(element).val(); + if (id !== '') { + return Api.group(id, callback); + } + }, + formatResult(object) { + return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; + }, + formatSelection(object) { + return object.full_name; + }, + dropdownCssClass: 'ajax-groups-dropdown select2-infinite', + // we do not want to escape markup since we are displaying html in results + escapeMarkup(m) { + return m; + }, + }); - return GroupsSelect; -})(); + $select.on('select2-loaded', () => { + const dropdown = document.querySelector('.select2-infinite .select2-results'); + dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; + }); + }); +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 52715fba43f..38403fdaf6e 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -55,7 +55,6 @@ import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; import './gl_form'; -import './groups_select'; import './header'; import './importer_status'; import './issuable_index'; diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index baf43190d9e..0f13221b81e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -9,8 +9,8 @@ import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteForm from './issue_note_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; export default { diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index aecd1f957e5..5c9119644e3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -5,10 +5,10 @@ import * as constants from '../constants'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; - import issueSystemNote from './issue_system_note.vue'; + import systemNote from '../../vue_shared/components/notes/system_note.vue'; import issueCommentForm from './issue_comment_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { @@ -37,7 +37,7 @@ components: { issueNote, issueDiscussion, - issueSystemNote, + systemNote, issueCommentForm, loadingIcon, placeholderNote, @@ -68,7 +68,7 @@ } return placeholderNote; } else if (note.individual_note) { - return note.notes[0].system ? issueSystemNote : issueNote; + return note.notes[0].system ? systemNote : issueNote; } return issueDiscussion; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index b8a96b23012..be37dd87de9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -286,6 +286,7 @@ export default { <input id="remove-source-branch-input" v-model="removeSourceBranch" + class="js-remove-source-branch-checkbox" :disabled="isRemoveSourceBranchButtonDisabled" type="checkbox"/> Remove source branch </label> @@ -311,8 +312,8 @@ export default { </button> </template> <template v-else> - <span class="bold"> - The pipeline for this merge request has not succeeded yet + <span class="bold js-resolve-mr-widget-items-message"> + You can only merge once the items above are resolved </span> </template> </div> diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 6921d91372f..e467ca56704 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -1,9 +1,26 @@ <script> + /** + * Common component to render a placeholder note and user information. + * + * This component needs to be used with a vuex store. + * That vuex store needs to have a `getUserData` getter that contains + * { + * path: String, + * avatar_url: String, + * name: String, + * username: String, + * } + * + * @example + * <placeholder-note + * :note="{body: 'This is a note'}" + * /> + */ import { mapGetters } from 'vuex'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { - name: 'issuePlaceholderNote', + name: 'placeholderNote', props: { note: { type: Object, diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index 80a8ef56a83..d805fea8006 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -1,4 +1,12 @@ <script> + /** + * Common component to render a placeholder system note. + * + * @example + * <placeholder-system-note + * :note="{ body: 'Commands are being applied'}" + * /> + */ export default { name: 'placeholderSystemNote', props: { diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 0cfb6522e77..98f8f32557d 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -1,6 +1,24 @@ <script> + /** + * Common component to render a system note, icon and user information. + * + * This component needs to be used with a vuex store. + * That vuex store needs to have a `targetNoteHash` getter + * + * @example + * <system-note + * :note="{ + * id: String, + * author: Object, + * createdAt: String, + * note_html: String, + * system_note_icon_name: String + * }" + * /> + */ import { mapGetters } from 'vuex'; - import issueNoteHeader from './issue_note_header.vue'; + import issueNoteHeader from '../../../notes/components/issue_note_header.vue'; + import { spriteIcon } from '../../../lib/utils/common_utils'; export default { name: 'systemNote', @@ -24,7 +42,7 @@ return this.targetNoteHash === this.noteAnchorId; }, iconHtml() { - return gl.utils.spriteIcon(this.note.system_note_icon_name); + return spriteIcon(this.note.system_note_icon_name); }, }, }; @@ -46,7 +64,8 @@ :author="note.author" :created-at="note.created_at" :note-id="note.id" - :action-text-html="note.note_html" /> + :action-text-html="note.note_html" + /> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 95898d54cf7..dc32e783258 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -12,12 +12,14 @@ :img-alt="tooltipText" :img-size="20" :tooltip-text="tooltipText" - tooltip-placement="top" + :tooltip-placement="top" + :username="username" /> */ import userAvatarImage from './user_avatar_image.vue'; +import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarLink', @@ -60,6 +62,22 @@ export default { required: false, default: 'top', }, + username: { + type: String, + required: false, + default: '', + }, + }, + computed: { + shouldShowUsername() { + return this.username.length > 0; + }, + avatarTooltipText() { + return this.shouldShowUsername ? '' : this.tooltipText; + }, + }, + directives: { + tooltip, }, }; </script> @@ -73,8 +91,13 @@ export default { :img-alt="imgAlt" :css-classes="imgCssClasses" :size="imgSize" - :tooltip-text="tooltipText" + :tooltip-text="avatarTooltipText" + :tooltip-placement="tooltipPlacement" + /><span + v-if="shouldShowUsername" + v-tooltip + :title="tooltipText" :tooltip-placement="tooltipPlacement" - /> + >{{username}}</span> </a> </template> diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b2847c348eb..0d80a85d521 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -65,7 +65,7 @@ display: flex; flex: 1; -webkit-flex: 1; - padding-left: 30px; + padding-left: 12px; position: relative; margin-bottom: 0; } @@ -221,10 +221,6 @@ box-shadow: 0 0 4px $search-input-focus-shadow-color; } - &.focus .fa-filter { - color: $common-gray-dark; - } - gl-emoji { display: inline-block; font-family: inherit; @@ -251,13 +247,6 @@ } } - .fa-filter { - position: absolute; - top: 10px; - left: 10px; - color: $gray-darkest; - } - .fa-times { right: 10px; color: $gray-darkest; diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 572915a4930..38f379dbf4f 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -57,6 +57,10 @@ class HelpController < ApplicationController def shortcuts end + def instance_configuration + @instance_configuration = InstanceConfiguration.new + end + def ui @user = User.new(id: 0, name: 'John Doe', username: '@johndoe') end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 7f03ce07dec..f28df83d5a5 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html do @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) + @merged_branch_names = + repository.merged_branch_names(@branches.map(&:name)) # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 Gitlab::GitalyClient.allow_n_plus_1_calls do @max_commits = @branches.reduce(0) do |memo, branch| diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b7a108a0ebd..fe1334c0cfe 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_action :authorize_update_issue!, only: [:update, :move] + before_action :authorize_update_issue!, only: [:edit, :update, :move] # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController respond_with(@issue) end + def edit + respond_with(@issue) + end + def show @noteable = @issue @note = @project.notes.new(noteable: @issue) @@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) respond_to do |format| + format.html do + recaptcha_check_with_fallback { render :edit } + end + format.json do render_issue_json end diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 533076585c0..852eac3647d 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -23,7 +23,7 @@ class BranchesFinder def filter_by_name(branches) if search - branches.select { |branch| branch.name.include?(search) } + branches.select { |branch| branch.name.upcase.include?(search.upcase) } else branches end diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb new file mode 100644 index 00000000000..cee319f20bc --- /dev/null +++ b/app/helpers/instance_configuration_helper.rb @@ -0,0 +1,18 @@ +module InstanceConfigurationHelper + def instance_configuration_cell_html(value, &block) + return '-' unless value.to_s.presence + + block_given? ? yield(value) : value + end + + def instance_configuration_host(host) + @instance_configuration_host ||= instance_configuration_cell_html(host).capitalize + end + + # Value must be in bytes + def instance_configuration_human_size_cell(value) + instance_configuration_cell_html(value) do |v| + number_to_human_size(v, strip_insignificant_zeros: true, significant: false) + end + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index cf3ce3c9e54..ca65e81f27a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -249,9 +249,7 @@ module Ci end def commit - @commit ||= project.commit(sha) - rescue - nil + @commit ||= project.commit_by(oid: sha) end def branch? diff --git a/app/models/environment.rb b/app/models/environment.rb index b6868ccbe8f..e613d21add6 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base end def ref_path - "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" + "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}" end def formatted_external_url diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb new file mode 100644 index 00000000000..b30b707e5fe --- /dev/null +++ b/app/models/instance_configuration.rb @@ -0,0 +1,71 @@ +require 'resolv' + +class InstanceConfiguration + SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze + SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze + CACHE_KEY = 'instance_configuration'.freeze + EXPIRATION_TIME = 24.hours + + def settings + @configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do + { ssh_algorithms_hashes: ssh_algorithms_hashes, + host: host, + gitlab_pages: gitlab_pages, + gitlab_ci: gitlab_ci }.deep_symbolize_keys + end + end + + private + + def ssh_algorithms_hashes + SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact + end + + def host + Settings.gitlab.host + end + + def gitlab_pages + Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host)) + end + + def resolv_dns(dns) + Resolv.getaddress(dns) + rescue Resolv::ResolvError + end + + def gitlab_ci + Settings.gitlab_ci + .to_h + .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes, + default: 100.megabytes }) + end + + def ssh_algorithm_file(algorithm) + File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub") + end + + def ssh_algorithm_hashes(algorithm) + content = ssh_algorithm_file_content(algorithm) + return unless content.present? + + { name: algorithm, + md5: ssh_algorithm_md5(content), + sha256: ssh_algorithm_sha256(content) } + end + + def ssh_algorithm_file_content(algorithm) + file = ssh_algorithm_file(algorithm) + return unless File.exist?(file) + + File.read(file) + end + + def ssh_algorithm_md5(ssh_file_content) + OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') + end + + def ssh_algorithm_sha256(ssh_file_content) + OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) + end +end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 5d798247863..2e824cda525 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' - after_create :update - after_save :update - after_destroy :update + after_create :update_daemon + after_save :update_daemon + after_destroy :update_daemon def to_param domain @@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base private - def update + def update_daemon ::Projects::UpdatePagesConfigurationService.new(project).execute end diff --git a/app/models/project.rb b/app/models/project.rb index 4689b588906..7185b4d44fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -540,6 +540,10 @@ class Project < ActiveRecord::Base repository.commit(ref) end + def commit_by(oid:) + repository.commit_by(oid: oid) + end + # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) latest_pipeline = pipelines.latest_successful_for(ref) @@ -553,7 +557,7 @@ class Project < ActiveRecord::Base def merge_base_commit(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id) - repository.commit(sha) if sha + commit_by(oid: sha) if sha end def saved? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 9ee3a533c1e..b487378edd2 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,6 +3,8 @@ class JiraService < IssueTrackerService validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true + validates :username, presence: true, if: :activated? + validates :password, presence: true, if: :activated? prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8ba07173c74..5c0b3338a62 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -153,7 +153,10 @@ class KubernetesService < DeploymentService end def default_namespace - "#{project.path}-#{project.id}" if project.present? + return unless project + + slug = "#{project.path}-#{project.id}".downcase + slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end def build_kubeclient!(api_path: 'api', api_version: 'v1') diff --git a/app/models/repository.rb b/app/models/repository.rb index 4324ea46aac..44a1e9ce529 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -76,6 +76,7 @@ class Repository @full_path = full_path @disk_path = disk_path || full_path @project = project + @commit_cache = {} end def ==(other) @@ -103,18 +104,17 @@ class Repository def commit(ref = 'HEAD') return nil unless exists? + return ref if ref.is_a?(::Commit) - commit = - if ref.is_a?(Gitlab::Git::Commit) - ref - else - Gitlab::Git::Commit.find(raw_repository, ref) - end + find_commit(ref) + end - commit = ::Commit.new(commit, @project) if commit - commit - rescue Rugged::OdbError, Rugged::TreeError - nil + # Finding a commit by the passed SHA + # Also takes care of caching, based on the SHA + def commit_by(oid:) + return @commit_cache[oid] if @commit_cache.key?(oid) + + @commit_cache[oid] = find_commit(oid) end def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) @@ -231,7 +231,7 @@ class Repository # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. def keep_around(sha) - return unless sha && commit(sha) + return unless sha && commit_by(oid: sha) return if kept_around?(sha) @@ -862,22 +862,12 @@ class Repository end def ff_merge(user, source, target_branch, merge_request: nil) - our_commit = rugged.branches[target_branch].target - their_commit = - if source.is_a?(Gitlab::Git::Commit) - source.raw_commit - else - rugged.lookup(source) - end - - raise 'Invalid merge target' if our_commit.nil? - raise 'Invalid merge source' if their_commit.nil? + their_commit_id = commit(source)&.id + raise 'Invalid merge source' if their_commit_id.nil? - with_branch(user, target_branch) do |start_commit| - merge_request&.update(in_progress_merge_commit_sha: their_commit.oid) + merge_request&.update(in_progress_merge_commit_sha: their_commit_id) - their_commit.oid - end + with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) } end def revert( @@ -912,18 +902,27 @@ class Repository end end - def merged_to_root_ref?(branch_name) - branch_commit = commit(branch_name) - root_ref_commit = commit(root_ref) + def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil) + branch = Gitlab::Git::Branch.find(self, branch_or_name) - if branch_commit - same_head = branch_commit.id == root_ref_commit.id - !same_head && ancestor?(branch_commit.id, root_ref_commit.id) + if branch + root_ref_sha = commit(root_ref).sha + same_head = branch.target == root_ref_sha + merged = + if pre_loaded_merged_branches + pre_loaded_merged_branches.include?(branch.name) + else + ancestor?(branch.target, root_ref_sha) + end + + !same_head && merged else nil end end + delegate :merged_branch_names, to: :raw_repository + def merge_base(first_commit_id, second_commit_id) first_commit_id = commit(first_commit_id).try(:id) || first_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id @@ -1031,6 +1030,10 @@ class Repository if instance_variable_defined?(ivar) instance_variable_get(ivar) else + # If the repository doesn't exist and a fallback was specified we return + # that value inmediately. This saves us Rugged/gRPC invocations. + return fallback unless fallback.nil? || exists? + begin value = if memoize_only @@ -1040,8 +1043,9 @@ class Repository end instance_variable_set(ivar, value) rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository - # if e.g. HEAD or the entire repository doesn't exist we want to - # gracefully handle this and not cache anything. + # Even if the above `#exists?` check passes these errors might still + # occur (for example because of a non-existing HEAD). We want to + # gracefully handle this and not cache anything fallback end end @@ -1069,6 +1073,18 @@ class Repository private + # TODO Generice finder, later split this on finders by Ref or Oid + # gitlab-org/gitlab-ce#39239 + def find_commit(oid_or_ref) + commit = if oid_or_ref.is_a?(Gitlab::Git::Commit) + oid_or_ref + else + Gitlab::Git::Commit.find(raw_repository, oid_or_ref) + end + + ::Commit.new(commit, @project) if commit + end + def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1107,12 +1123,12 @@ class Repository def last_commit_for_path_by_gitaly(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) - commit(c) + commit_by(oid: c) end def last_commit_for_path_by_rugged(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path) - commit(sha) + commit_by(oid: sha) end def last_commit_id_for_path_by_shelling_out(sha, path) diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index fd2ba9ac1ca..9038c4fbebd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -6,13 +6,13 @@ .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs = nav_link(page: [dashboard_projects_path, root_path]) do - = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects = nav_link(page: starred_dashboard_projects_path) do - = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + = link_to starred_dashboard_projects_path, data: {placement: 'right'} do Starred projects = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do - = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do + = link_to explore_root_path, data: {placement: 'right'} do Explore projects .nav-controls diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index c25eae63eec..d0c2e0b1d69 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -11,6 +11,7 @@ %span= Gitlab::VERSION %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) = version_status_badge + %p.slead GitLab is open source software to collaborate on code. %br @@ -23,6 +24,7 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. + %p= link_to 'Check the current instance configuration ', help_instance_configuration_url %hr .row.prepend-top-default diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml new file mode 100644 index 00000000000..f09e3825a4b --- /dev/null +++ b/app/views/help/instance_configuration.html.haml @@ -0,0 +1,17 @@ +- page_title 'Instance Configuration' +.wiki.documentation + %h1 Instance Configuration + + %p + In this page you will find information about the settings that are used in your current instance. + + = render 'help/instance_configuration/ssh_info' + = render 'help/instance_configuration/gitlab_pages' + = render 'help/instance_configuration/gitlab_ci' + %p + %strong Table of contents + + %ul + = content_for :table_content + + = content_for :settings_content diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml new file mode 100644 index 00000000000..7fa8bd086d4 --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml @@ -0,0 +1,24 @@ +- content_for :table_content do + %li= link_to 'GitLab CI', '#gitlab-ci' + +- content_for :settings_content do + %h2#gitlab-ci + GitLab CI + + %p + Below are the current settings regarding + = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') } + + .table-responsive + %table + %thead + %tr + %th Setting + %th= instance_configuration_host(@instance_configuration.settings[:host]) + %th Default + %tbody + %tr + - artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size] + %td Artifacts maximum size + %td= instance_configuration_human_size_cell(artifacts_size[:value]) + %td= instance_configuration_human_size_cell(artifacts_size[:default]) diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml new file mode 100644 index 00000000000..bdd77730dcc --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml @@ -0,0 +1,35 @@ +- gitlab_pages = @instance_configuration.settings[:gitlab_pages] +- content_for :table_content do + %li= link_to 'GitLab Pages', '#gitlab-pages' + +- content_for :settings_content do + %h2#gitlab-pages + GitLab Pages + + %p + Below are the settings for + = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') } + .table-responsive + %table + %thead + %tr + %th Setting + %th= instance_configuration_host(@instance_configuration.settings[:host]) + %tbody + %tr + %td Domain Name + %td + %code= instance_configuration_cell_html(gitlab_pages[:host]) + %tr + %td IP Address + %td + %code= instance_configuration_cell_html(gitlab_pages[:ip_address]) + %tr + %td Port + %td + %code= instance_configuration_cell_html(gitlab_pages[:port]) + %br + + %p + The maximum size of your Pages site is regulated by the artifacts maximum + size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }} diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml new file mode 100644 index 00000000000..987cc61b3f6 --- /dev/null +++ b/app/views/help/instance_configuration/_ssh_info.html.haml @@ -0,0 +1,27 @@ +- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes] +- if ssh_info.any? + - content_for :table_content do + %li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints' + + - content_for :settings_content do + %h2#ssh-host-keys-fingerprints + SSH host keys fingerprints + + %p + Below are the fingerprints for the current instance SSH host keys. + + .table-responsive + %table + %thead + %tr + %th Algorithm + %th MD5 + %th SHA256 + %tbody + - ssh_info.each do |algorithm| + %tr + %td= algorithm[:name] + %td + %code= instance_configuration_cell_html(algorithm[:md5]) + %td + %code= instance_configuration_cell_html(algorithm[:sha256]) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 49101d1efa4..6e02ae6c9cc 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,3 +1,4 @@ +- merged = local_assigns.fetch(:merged, false) - commit = @repository.commit(branch.dereferenced_target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) @@ -12,7 +13,7 @@ - if branch.name == @repository.root_ref %span.label.label-primary default - - elsif @repository.merged_to_root_ref? branch.name + - elsif merged %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } = s_('Branches|merged') @@ -47,7 +48,7 @@ target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), branch_name: branch.name, - is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } + is_merged: ("true" if merged) } } = icon("trash-o") - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 7d9645d79e6..aade310236e 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -38,7 +38,7 @@ - if @branches.any? %ul.content-list.all-branches - @branches.each do |branch| - = render "projects/branches/branch", branch: branch + = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names) = paginate @branches, theme: 'gitlab' - else .nothing-here-block diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index ff76abc3553..b127e06030e 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -47,7 +47,7 @@ - if can?(current_user, :update_cluster, @cluster) .form-group - = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success' + = field.submit _('Save'), class: 'btn btn-success' %section.settings#js-cluster-details .settings-header @@ -68,7 +68,7 @@ %section.settings#js-cluster-advanced-settings .settings-header - %h4= s_('ClusterIntegration|Advanced settings') + %h4= _('Advanced settings') %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index c9956183e12..af564b93dc3 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -72,6 +72,7 @@ %pre.light-well :preserve cd existing_repo + git remote rename origin old-origin git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git push -u origin --all git push -u origin --tags diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml new file mode 100644 index 00000000000..1b7d878c38c --- /dev/null +++ b/app/views/projects/issues/edit.html.haml @@ -0,0 +1,7 @@ +- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" + +%h3.page-title + Edit Issue ##{@issue.iid} +%hr + += render "form" diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 161b1c9fd72..fabb17c7340 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,6 @@ %ul.tokens-container.list-unstyled %li.input-token %input.form-control.filtered-search{ search_filter_input_options(type) } - = icon('filter') #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { action: 'submit' } } |