summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore1
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock62
-rw-r--r--PROCESS.md5
-rw-r--r--app/assets/javascripts/gl_dropdown.js1
-rw-r--r--app/assets/javascripts/locale/index.js3
-rw-r--r--app/assets/javascripts/locale/sprintf.js26
-rw-r--r--app/assets/javascripts/registry/components/app.vue62
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue131
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue137
-rw-r--r--app/assets/javascripts/registry/constants.js15
-rw-r--r--app/assets/javascripts/registry/index.js25
-rw-r--r--app/assets/javascripts/registry/stores/actions.js39
-rw-r--r--app/assets/javascripts/registry/stores/getters.js2
-rw-r--r--app/assets/javascripts/registry/stores/index.js39
-rw-r--r--app/assets/javascripts/registry/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js54
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue32
-rw-r--r--app/assets/javascripts/vue_shared/translate.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/new-nav.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss8
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss4
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb21
-rw-r--r--app/controllers/projects/registry/tags_controller.rb27
-rw-r--r--app/controllers/projects/wikis_controller.rb16
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/project_wiki.rb57
-rw-r--r--app/models/wiki_page.rb10
-rw-r--r--app/presenters/merge_request_presenter.rb2
-rw-r--r--app/serializers/container_repositories_serializer.rb3
-rw-r--r--app/serializers/container_repository_entity.rb25
-rw-r--r--app/serializers/container_tag_entity.rb23
-rw-r--r--app/serializers/container_tags_serializer.rb17
-rw-r--r--app/serializers/merge_request_entity.rb2
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml4
-rw-r--r--app/views/projects/registry/repositories/_image.html.haml32
-rw-r--r--app/views/projects/registry/repositories/index.html.haml93
-rw-r--r--app/views/projects/wikis/history.html.haml4
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--changelogs/unreleased/37970-timestamped-ci.yml5
-rw-r--r--changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml5
-rw-r--r--changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml5
-rw-r--r--changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml5
-rw-r--r--changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml5
-rw-r--r--changelogs/unreleased/winh-sprintf.yml5
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb39
-rw-r--r--db/schema.rb2
-rw-r--r--doc/development/i18n_guide.md11
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/banzai/filter/sanitization_filter.rb3
-rw-r--r--lib/gitlab/ci/ansi2html.rb13
-rw-r--r--lib/gitlab/git/repository.rb8
-rw-r--r--lib/gitlab/git/user.rb5
-rw-r--r--lib/gitlab/git/wiki.rb115
-rw-r--r--lib/gitlab/git/wiki_file.rb19
-rw-r--r--lib/gitlab/git/wiki_page.rb39
-rw-r--r--lib/gitlab/git/wiki_page_version.rb19
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb14
-rw-r--r--lib/gitlab/url_sanitizer.rb8
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock17
-rw-r--r--qa/qa/page/admin/menu.rb2
-rw-r--r--qa/qa/page/project/show.rb4
-rw-r--r--qa/qa/specs/config.rb3
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb12
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb44
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb90
-rw-r--r--spec/features/container_registry_spec.rb9
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json2
-rw-r--r--spec/fixtures/api/schemas/registry/repositories.json6
-rw-r--r--spec/fixtures/api/schemas/registry/repository.json27
-rw-r--r--spec/fixtures/api/schemas/registry/tag.json28
-rw-r--r--spec/fixtures/api/schemas/registry/tags.json6
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js (renamed from spec/javascripts/notes/stores/helpers.js)0
-rw-r--r--spec/javascripts/locale/sprintf_spec.js74
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js2
-rw-r--r--spec/javascripts/registry/components/app_spec.js122
-rw-r--r--spec/javascripts/registry/components/collapsible_container_spec.js58
-rw-r--r--spec/javascripts/registry/components/table_registry_spec.js49
-rw-r--r--spec/javascripts/registry/getters_spec.js43
-rw-r--r--spec/javascripts/registry/mock_data.js122
-rw-r--r--spec/javascripts/registry/stores/actions_spec.js85
-rw-r--r--spec/javascripts/registry/stores/mutations_spec.js81
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb26
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb28
-rw-r--r--spec/lib/gitlab/git/user_spec.rb14
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb41
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb25
-rw-r--r--spec/models/merge_request_spec.rb43
-rw-r--r--spec/models/project_wiki_spec.rb64
-rw-r--r--spec/models/repository_spec.rb113
-rw-r--r--spec/models/wiki_page_spec.rb14
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb4
-rw-r--r--spec/requests/api/runner_spec.rb3
-rw-r--r--spec/serializers/container_repository_entity_spec.rb41
-rw-r--r--spec/serializers/container_tag_entity_spec.rb43
-rw-r--r--spec/serializers/merge_request_entity_spec.rb10
-rw-r--r--spec/services/projects/create_service_spec.rb11
-rw-r--r--spec/support/stub_gitlab_calls.rb4
-rw-r--r--spec/views/projects/registry/repositories/index.html.haml_spec.rb36
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb7
119 files changed, 2361 insertions, 424 deletions
diff --git a/.flayignore b/.flayignore
index b63ce4c4df0..acac0ce14c9 100644
--- a/.flayignore
+++ b/.flayignore
@@ -5,3 +5,4 @@ app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
+lib/gitlab/gitaly_client/operation_service.rb
diff --git a/Gemfile b/Gemfile
index 55de8d02922..44c459c497f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -105,7 +105,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0'
# for Google storage
-gem 'google-api-client', '~> 0.8.6'
+gem 'google-api-client', '~> 0.13.6'
# for aws storage
gem 'unf', '~> 0.1.4'
@@ -239,7 +239,7 @@ gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6'
gem 'uglifier', '~> 2.7.2'
-gem 'addressable', '~> 2.3.8'
+gem 'addressable', '~> 2.5.2'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
@@ -356,7 +356,7 @@ end
group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0'
- gem 'json-schema', '~> 2.6.2'
+ gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index e1bb5d0854d..a0ad2716c01 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -45,7 +45,8 @@ GEM
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
- addressable (2.3.8)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
@@ -62,10 +63,6 @@ GEM
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
- autoparse (0.3.3)
- addressable (>= 2.3.1)
- extlib (>= 0.9.15)
- multi_json (>= 1.0.0)
autoprefixer-rails (6.2.3)
execjs
json
@@ -146,6 +143,8 @@ GEM
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0)
html-pipeline
+ declarative (0.0.10)
+ declarative-option (0.1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
@@ -188,7 +187,6 @@ GEM
excon (0.57.1)
execjs (2.6.0)
expression_parser (0.9.0)
- extlib (0.9.16)
factory_girl (4.7.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.7.0)
@@ -288,10 +286,10 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-grit (2.8.1)
+ gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
- mime-types (>= 1.16, < 3)
+ mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.2)
gitlab_omniauth-ldap (2.0.4)
@@ -319,20 +317,16 @@ GEM
json
multi_json
request_store (>= 1.0)
- google-api-client (0.8.7)
- activesupport (>= 3.2, < 5.0)
- addressable (~> 2.3)
- autoparse (~> 0.3)
- extlib (~> 0.9)
- faraday (~> 0.9)
- googleauth (~> 0.3)
- launchy (~> 2.4)
- multi_json (~> 1.10)
- retriable (~> 1.4)
- signet (~> 0.6)
+ google-api-client (0.13.6)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (~> 0.5)
+ httpclient (>= 2.8.1, < 3.0)
+ mime-types (~> 3.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.0)
google-protobuf (3.4.0.2)
- googleauth (0.5.1)
- faraday (~> 0.9)
+ googleauth (0.5.3)
+ faraday (~> 0.12)
jwt (~> 1.4)
logging (~> 2.0)
memoist (~> 0.12)
@@ -422,8 +416,8 @@ GEM
multi_json (>= 1.3)
securecompare
url_safe_base64
- json-schema (2.6.2)
- addressable (~> 2.3.8)
+ json-schema (2.8.0)
+ addressable (>= 2.4)
jwt (1.5.6)
kaminari (1.0.1)
activesupport (>= 4.1.0)
@@ -475,18 +469,20 @@ GEM
mail (2.6.6)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
- memoist (0.15.0)
+ memoist (0.16.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
- mime-types (2.99.3)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_mime (0.1.4)
mini_portile2 (2.3.0)
minitest (5.7.0)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
- multi_json (1.12.1)
+ multi_json (1.12.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (1.0.0)
@@ -635,6 +631,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
+ public_suffix (3.0.0)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.8)
rack-accept (0.4.5)
@@ -717,6 +714,10 @@ GEM
redis-store (~> 1.2.0)
redis-store (1.2.0)
redis (>= 2.2)
+ representable (3.0.4)
+ declarative (< 0.1.0)
+ declarative-option (< 0.2.0)
+ uber (< 0.2.0)
request_store (1.3.1)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
@@ -724,7 +725,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
- retriable (1.4.1)
+ retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.2.1)
@@ -903,6 +904,7 @@ GEM
tzinfo (1.2.3)
thread_safe (~> 0.1)
u2f (0.2.1)
+ uber (0.1.0)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
@@ -963,7 +965,7 @@ DEPENDENCIES
ace-rails-ap (~> 4.1.0)
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
- addressable (~> 2.3.8)
+ addressable (~> 2.5.2)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
@@ -1033,7 +1035,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
- google-api-client (~> 0.8.6)
+ google-api-client (~> 0.13.6)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
@@ -1051,7 +1053,7 @@ DEPENDENCIES
jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
- json-schema (~> 2.6.2)
+ json-schema (~> 2.8.0)
jwt (~> 1.5.6)
kaminari (~> 1.0)
knapsack (~> 1.11.0)
diff --git a/PROCESS.md b/PROCESS.md
index ed4e84dd0b6..5e65bb59246 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
+A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc)
+and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc),
+just like any other issue, to help GitLab team members focus on issues that are
+relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on).
+
## Release retrospective and kickoff
- [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective)
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 50d822eba5a..ff218ccad62 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -548,6 +548,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.positionMenuAbove = function() {
var $menu = this.dropdown.find('.dropdown-menu');
+ $menu.addClass('dropdown-open-top');
$menu.css('top', 'initial');
$menu.css('bottom', '100%');
};
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 6a5084efeb8..af718e894cf 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,5 +1,7 @@
import Jed from 'jed';
+import sprintf from './sprintf';
+
/**
This is required to require all the translation folders in the current directory
this saves us having to do this manually & keep up to date with new languages
@@ -66,4 +68,5 @@ export { lang };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
+export { sprintf };
export default locale;
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
new file mode 100644
index 00000000000..5f4a053f98e
--- /dev/null
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -0,0 +1,26 @@
+import _ from 'underscore';
+
+/**
+ Very limited implementation of sprintf supporting only named parameters.
+
+ @param input (translated) text with parameters (e.g. '%{num_users} users use us')
+ @param parameters object mapping parameter names to values (e.g. { num_users: 5 })
+ @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
+ @returns {String} the text with parameters replaces (e.g. '5 users use us')
+
+ @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
+ @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
+**/
+export default (input, parameters, escapeParameters = true) => {
+ let output = input;
+
+ if (parameters) {
+ Object.keys(parameters).forEach((parameterName) => {
+ const parameterValue = parameters[parameterName];
+ const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue;
+ output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue);
+ });
+ }
+
+ return output;
+};
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
new file mode 100644
index 00000000000..2d8ca443ea7
--- /dev/null
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -0,0 +1,62 @@
+<script>
+ /* globals Flash */
+ import { mapGetters, mapActions } from 'vuex';
+ import '../../flash';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import store from '../stores';
+ import collapsibleContainer from './collapsible_container.vue';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ name: 'registryListApp',
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ store,
+ components: {
+ collapsibleContainer,
+ loadingIcon,
+ },
+ computed: {
+ ...mapGetters([
+ 'isLoading',
+ 'repos',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'setMainEndpoint',
+ 'fetchRepos',
+ ]),
+ },
+ created() {
+ this.setMainEndpoint(this.endpoint);
+ },
+ mounted() {
+ this.fetchRepos()
+ .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
+ },
+ };
+</script>
+<template>
+ <div>
+ <loading-icon
+ v-if="isLoading"
+ size="3"
+ />
+
+ <collapsible-container
+ v-else-if="!isLoading && repos.length"
+ v-for="(item, index) in repos"
+ :key="index"
+ :repo="item"
+ />
+
+ <p v-else-if="!isLoading && !repos.length">
+ {{__("No container images stored for this project. Add one by following the instructions above.")}}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
new file mode 100644
index 00000000000..41ea9742406
--- /dev/null
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -0,0 +1,131 @@
+<script>
+ /* globals Flash */
+ import { mapActions } from 'vuex';
+ import '../../flash';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import tableRegistry from './table_registry.vue';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ name: 'collapsibeContainerRegisty',
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ clipboardButton,
+ loadingIcon,
+ tableRegistry,
+ },
+ directives: {
+ tooltip,
+ },
+ data() {
+ return {
+ isOpen: false,
+ };
+ },
+ computed: {
+ clipboardText() {
+ return `docker pull ${this.repo.location}`;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'fetchRepos',
+ 'fetchList',
+ 'deleteRepo',
+ ]),
+
+ toggleRepo() {
+ this.isOpen = !this.isOpen;
+
+ if (this.isOpen) {
+ this.fetchList({ repo: this.repo })
+ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
+ }
+ },
+
+ handleDeleteRepository() {
+ this.deleteRepo(this.repo)
+ .then(() => this.fetchRepos())
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
+ },
+
+ showError(message) {
+ Flash((errorMessages[message]));
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="container-image">
+ <div
+ class="container-image-head">
+ <button
+ type="button"
+ @click="toggleRepo"
+ class="js-toggle-repo btn-link">
+ <i
+ class="fa"
+ :class="{
+ 'fa-chevron-right': !isOpen,
+ 'fa-chevron-up': isOpen,
+ }"
+ aria-hidden="true">
+ </i>
+ {{repo.name}}
+ </button>
+
+ <clipboard-button
+ v-if="repo.location"
+ :text="clipboardText"
+ :title="repo.location"
+ />
+
+ <div class="controls hidden-xs pull-right">
+ <button
+ v-if="repo.canDelete"
+ type="button"
+ class="js-remove-repo btn btn-danger"
+ :title="s__('ContainerRegistry|Remove repository')"
+ :aria-label="s__('ContainerRegistry|Remove repository')"
+ v-tooltip
+ @click="handleDeleteRepository">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </div>
+
+ </div>
+
+ <loading-icon
+ v-if="repo.isLoading"
+ class="append-bottom-20"
+ size="2"
+ />
+
+ <div
+ v-else-if="!repo.isLoading && isOpen"
+ class="container-image-tags">
+
+ <table-registry
+ v-if="repo.list.length"
+ :repo="repo"
+ />
+
+ <div
+ v-else
+ class="nothing-here-block">
+ {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
new file mode 100644
index 00000000000..4ce1571b0aa
--- /dev/null
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -0,0 +1,137 @@
+<script>
+ /* globals Flash */
+ import { mapActions } from 'vuex';
+ import { n__ } from '../../locale';
+ import '../../flash';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import tablePagination from '../../vue_shared/components/table_pagination.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import timeagoMixin from '../../vue_shared/mixins/timeago';
+ import { errorMessages, errorMessagesTypes } from '../constants';
+
+ export default {
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ clipboardButton,
+ tablePagination,
+ },
+ mixins: [
+ timeagoMixin,
+ ],
+ directives: {
+ tooltip,
+ },
+ computed: {
+ shouldRenderPagination() {
+ return this.repo.pagination.total > this.repo.pagination.perPage;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'fetchList',
+ 'deleteRegistry',
+ ]),
+
+ layers(item) {
+ return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
+ },
+
+ handleDeleteRegistry(registry) {
+ this.deleteRegistry(registry)
+ .then(() => this.fetchList({ repo: this.repo }))
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ },
+
+ onPageChange(pageNumber) {
+ this.fetchList({ repo: this.repo, page: pageNumber })
+ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
+ },
+
+ clipboardText(text) {
+ return `docker pull ${text}`;
+ },
+
+ showError(message) {
+ Flash((errorMessages[message]));
+ },
+ },
+ };
+</script>
+<template>
+<div>
+ <table class="table tags">
+ <thead>
+ <tr>
+ <th>{{s__('ContainerRegistry|Tag')}}</th>
+ <th>{{s__('ContainerRegistry|Tag ID')}}</th>
+ <th>{{s__("ContainerRegistry|Size")}}</th>
+ <th>{{s__("ContainerRegistry|Created")}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="(item, i) in repo.list"
+ :key="i">
+ <td>
+
+ {{item.tag}}
+
+ <clipboard-button
+ v-if="item.location"
+ :title="item.location"
+ :text="clipboardText(item.location)"
+ />
+ </td>
+ <td>
+ <span
+ v-tooltip
+ :title="item.revision"
+ data-placement="bottom">
+ {{item.shortRevision}}
+ </span>
+ </td>
+ <td>
+ {{item.size}}
+ <template v-if="item.size && item.layers">
+ &middot;
+ </template>
+ {{layers(item)}}
+ </td>
+
+ <td>
+ {{timeFormated(item.createdAt)}}
+ </td>
+
+ <td class="content">
+ <button
+ v-if="item.canDelete"
+ type="button"
+ class="js-delete-registry btn btn-danger hidden-xs pull-right"
+ :title="s__('ContainerRegistry|Remove tag')"
+ :aria-label="s__('ContainerRegistry|Remove tag')"
+ data-container="body"
+ v-tooltip
+ @click="handleDeleteRegistry(item)">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onPageChange"
+ :page-info="repo.pagination"
+ />
+</div>
+</template>
diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js
new file mode 100644
index 00000000000..712b0fade3d
--- /dev/null
+++ b/app/assets/javascripts/registry/constants.js
@@ -0,0 +1,15 @@
+import { __ } from '../locale';
+
+export const errorMessagesTypes = {
+ FETCH_REGISTRY: 'FETCH_REGISTRY',
+ FETCH_REPOS: 'FETCH_REPOS',
+ DELETE_REPO: 'DELETE_REPO',
+ DELETE_REGISTRY: 'DELETE_REGISTRY',
+};
+
+export const errorMessages = {
+ [errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
+ [errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
+ [errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
+ [errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
+};
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
new file mode 100644
index 00000000000..d8edff73f72
--- /dev/null
+++ b/app/assets/javascripts/registry/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import registryApp from './components/app.vue';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#js-vue-registry-images',
+ components: {
+ registryApp,
+ },
+ data() {
+ const dataset = document.querySelector(this.$options.el).dataset;
+ return {
+ endpoint: dataset.endpoint,
+ };
+ },
+ render(createElement) {
+ return createElement('registry-app', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
new file mode 100644
index 00000000000..34ed40b8b65
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import * as types from './mutation_types';
+
+Vue.use(VueResource);
+
+export const fetchRepos = ({ commit, state }) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+
+ return Vue.http.get(state.endpoint)
+ .then(res => res.json())
+ .then((response) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+ commit(types.SET_REPOS_LIST, response);
+ });
+};
+
+export const fetchList = ({ commit }, { repo, page }) => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+
+ return Vue.http.get(repo.tagsPath, { params: { page } })
+ .then((response) => {
+ const headers = response.headers;
+
+ return response.json().then((resp) => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+ commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
+ });
+ });
+};
+
+export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
+ .then(res => res.json());
+
+export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
+ .then(res => res.json());
+
+export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
+export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
new file mode 100644
index 00000000000..588f479c492
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -0,0 +1,2 @@
+export const isLoading = state => state.isLoading;
+export const repos = state => state.repos;
diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js
new file mode 100644
index 00000000000..78b67881210
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/index.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: {
+ isLoading: false,
+ endpoint: '', // initial endpoint to fetch the repos list
+ /**
+ * Each object in `repos` has the following strucure:
+ * {
+ * name: String,
+ * isLoading: Boolean,
+ * tagsPath: String // endpoint to request the list
+ * destroyPath: String // endpoit to delete the repo
+ * list: Array // List of the registry images
+ * }
+ *
+ * Each registry image inside `list` has the following structure:
+ * {
+ * tag: String,
+ * revision: String
+ * shortRevision: String
+ * size: Number
+ * layers: Number
+ * createdAt: String
+ * destroyPath: String // endpoit to delete each image
+ * }
+ */
+ repos: [],
+ },
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js
new file mode 100644
index 00000000000..2c69bf11807
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/mutation_types.js
@@ -0,0 +1,7 @@
+export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
+
+export const SET_REPOS_LIST = 'SET_REPOS_LIST';
+export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
+
+export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST';
+export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING';
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
new file mode 100644
index 00000000000..e40382e7afc
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -0,0 +1,54 @@
+import * as types from './mutation_types';
+import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
+
+export default {
+
+ [types.SET_MAIN_ENDPOINT](state, endpoint) {
+ Object.assign(state, { endpoint });
+ },
+
+ [types.SET_REPOS_LIST](state, list) {
+ Object.assign(state, {
+ repos: list.map(el => ({
+ canDelete: !!el.destroy_path,
+ destroyPath: el.destroy_path,
+ id: el.id,
+ isLoading: false,
+ list: [],
+ location: el.location,
+ name: el.path,
+ tagsPath: el.tags_path,
+ })),
+ });
+ },
+
+ [types.TOGGLE_MAIN_LOADING](state) {
+ Object.assign(state, { isLoading: !state.isLoading });
+ },
+
+ [types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
+ const listToUpdate = state.repos.find(el => el.id === repo.id);
+
+ const normalizedHeaders = normalizeHeaders(headers);
+ const pagination = parseIntPagination(normalizedHeaders);
+
+ listToUpdate.pagination = pagination;
+
+ listToUpdate.list = resp.map(element => ({
+ tag: element.name,
+ revision: element.revision,
+ shortRevision: element.short_revision,
+ size: element.size,
+ layers: element.layers,
+ location: element.location,
+ createdAt: element.created_at,
+ destroyPath: element.destroy_path,
+ canDelete: !!element.destroy_path,
+ }));
+ },
+
+ [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
+ const listToUpdate = state.repos.find(el => el.id === list.id);
+ listToUpdate.isLoading = !listToUpdate.isLoading;
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
index aaf9d3304a4..09561694939 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="loading" showDisabledButton />
+ <status-icon status="loading" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Checking ability to merge automatically
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
index dc252f8a9b7..5d468a085cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -12,7 +12,7 @@ export default {
<div class="mr-widget-body media">
<status-icon
status="failed"
- showDisabledButton />
+ :show-disabled-button="true" />
<div class="media-body space-children">
<span
v-if="mr.shouldBeRebased"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index 1cb24549d53..c25d6c359bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -51,7 +51,7 @@ export default {
</span>
</template>
<template v-else>
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 9f0a359d01a..1bc0b7e0819 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
index 797511d4e3a..00047718201 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" showDisabledButton />
+ <status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 167a0d4613a..1cedf86e811 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index c5be9a0530a..6853ba4b9f8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index 89f38e5bd2a..af19cf6ab87 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index d762ca6e640..a119ecbbdfe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -10,7 +10,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" showDisabledButton />
+ <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index b11a06899cf..54be1fbe675 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -38,7 +38,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
+ <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
new file mode 100644
index 00000000000..3a7143c450e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -0,0 +1,32 @@
+<script>
+ /**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ */
+
+ export default {
+ name: 'clipboardButton',
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn btn-transparent btn-clipboard"
+ :data-title="title"
+ :data-clipboard-text="text">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard">
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js
index f83c4b00761..2c7886ec308 100644
--- a/app/assets/javascripts/vue_shared/translate.js
+++ b/app/assets/javascripts/vue_shared/translate.js
@@ -2,6 +2,7 @@ import {
__,
n__,
s__,
+ sprintf,
} from '../locale';
export default (Vue) => {
@@ -37,6 +38,7 @@ export default (Vue) => {
@returns {String} Translated context based text
**/
s__,
+ sprintf,
},
});
};
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5c397470629..fa92d4ccf4f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -745,6 +745,10 @@
#{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
+ &.dropdown-open-top {
+ margin-bottom: $dropdown-vertical-offset;
+ }
+
li {
display: block;
padding: 0 1px;
diff --git a/app/assets/stylesheets/framework/new-nav.scss b/app/assets/stylesheets/framework/new-nav.scss
index 3abf3e4ac7d..7899be2c2d3 100644
--- a/app/assets/stylesheets/framework/new-nav.scss
+++ b/app/assets/stylesheets/framework/new-nav.scss
@@ -295,7 +295,7 @@ header.navbar-gitlab-new {
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
- margin-top: 4px;
+ margin-top: $dropdown-vertical-offset;
}
.breadcrumbs {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e8bb42f4a8c..9bbda87dec9 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -327,6 +327,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns
*/
$dropdown-width: 300px;
+$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index 3266714396e..dfff3e15556 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -9,6 +9,14 @@
.container-image-head {
padding: 0 16px;
line-height: 4em;
+
+ .btn-link {
+ padding: 0;
+
+ &:focus {
+ outline: none;
+ }
+ }
}
.table.tags {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index be4db597689..74d9acb5490 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -362,7 +362,7 @@
.dropdown-menu {
top: initial;
- bottom: 40px;
+ bottom: 100%;
width: 298px;
}
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index fe22d186af1..a355e2dee24 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -12,3 +12,7 @@
margin-left: 10px;
}
}
+
+.registry-placeholder {
+ min-height: 60px;
+}
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index 71e7dc70a4d..32c0fc6d14a 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -6,17 +6,26 @@ module Projects
def index
@images = project.container_repositories
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: ContainerRepositoriesSerializer
+ .new(project: project, current_user: current_user)
+ .represent(@images)
+ end
+ end
end
def destroy
if image.destroy
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- notice: 'Image repository has been removed successfully!'
+ respond_to do |format|
+ format.json { head :no_content }
+ end
else
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- alert: 'Failed to remove image repository!'
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
end
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index ae72bd03cfb..e602aa3f393 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -3,20 +3,35 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy]
+ def index
+ respond_to do |format|
+ format.json do
+ render json: ContainerTagsSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ .represent(tags)
+ end
+ end
+ end
+
def destroy
if tag.delete
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- notice: 'Registry tag has been removed successfully!'
+ respond_to do |format|
+ format.json { head :no_content }
+ end
else
- redirect_to project_container_registry_index_path(@project),
- status: 302,
- alert: 'Failed to remove registry tag!'
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
end
end
private
+ def tags
+ Kaminari::PaginatableArray.new(image.tags, limit: 15)
+ end
+
def image
@image ||= project.container_repositories
.find(params[:repository_id])
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 968d880886c..a8ebdf5a4a9 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController
response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
- if file.on_disk?
- send_file file.on_disk_path, disposition: 'inline'
- else
- send_data(
- file.raw_data,
- type: file.mime_type,
- disposition: 'inline',
- filename: file.name
- )
- end
+ send_data(
+ file.raw_data,
+ type: file.mime_type,
+ disposition: 'inline',
+ filename: file.name
+ )
else
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ee544d8ac56..dd315866e60 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -229,6 +229,10 @@ module Ci
variables
end
+ def features
+ { trace_sections: true }
+ end
+
def merge_request
return @merge_request if defined?(@merge_request)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e85b83daf9e..3cb6913549d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -742,10 +742,9 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
- has_ci_integration = source_project.try(:ci_service)
- uses_gitlab_ci = all_pipelines.any?
+ return false if has_no_commits?
- (has_ci_integration || uses_gitlab_ci) && commits.any?
+ !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
end
def branch_missing?
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index c4cc1c1cf22..bb7be29ef66 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -54,12 +54,15 @@ class ProjectWiki
[Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('')
end
- # Returns the Gollum::Wiki object.
+ # Returns the Gitlab::Git::Wiki object.
def wiki
@wiki ||= begin
- Gollum::Wiki.new(path_to_repo)
- rescue Rugged::OSError
- create_repo!
+ gl_repository = Gitlab::GlRepository.gl_repository(project, true)
+ raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository)
+
+ create_repo!(raw_repository) unless raw_repository.exists?
+
+ Gitlab::Git::Wiki.new(raw_repository)
end
end
@@ -86,20 +89,14 @@ class ProjectWiki
# Returns an initialized WikiPage instance or nil
def find_page(title, version = nil)
page_title, page_dir = page_title_and_dir(title)
- if page = wiki.page(page_title, version, page_dir)
+
+ if page = wiki.page(title: page_title, version: version, dir: page_dir)
WikiPage.new(self, page, true)
- else
- nil
end
end
- def find_file(name, version = nil, try_on_disk = true)
- version = wiki.ref if version.nil? # Gollum::Wiki#file ?
- if wiki_file = wiki.file(name, version, try_on_disk)
- wiki_file
- else
- nil
- end
+ def find_file(name, version = nil)
+ wiki.file(name, version)
end
def create_page(title, content, format = :markdown, message = nil)
@@ -108,7 +105,7 @@ class ProjectWiki
wiki.write_page(title, format.to_sym, content, commit)
update_project_activity
- rescue Gollum::DuplicatePageError => e
+ rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
return false
end
@@ -116,13 +113,13 @@ class ProjectWiki
def update_page(page, content:, title: nil, format: :markdown, message: nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, title || page.name, format.to_sym, content, commit)
+ wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
update_project_activity
end
def delete_page(page, message = nil)
- wiki.delete_page(page, commit_details(:deleted, message, page.title))
+ wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_project_activity
end
@@ -145,20 +142,8 @@ class ProjectWiki
wiki.class.default_ref
end
- def create_repo!
- if init_repo(disk_path)
- wiki = Gollum::Wiki.new(path_to_repo)
- else
- raise CouldNotCreateWikiError
- end
-
- repository.after_create
-
- wiki
- end
-
def ensure_repository
- create_repo! unless repository_exists?
+ raise CouldNotCreateWikiError unless wiki.repository_exists?
end
def hook_attrs
@@ -173,24 +158,24 @@ class ProjectWiki
private
- def init_repo(disk_path)
+ def create_repo!(raw_repository)
gitlab_shell.add_repository(project.repository_storage, disk_path)
+
+ raise CouldNotCreateWikiError unless raw_repository.exists?
+
+ repository.after_create
end
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
- { email: @user.email, name: @user.name, message: commit_message }
+ Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
end
def default_message(action, title)
"#{@user.username} #{action} page: #{title}"
end
- def path_to_repo
- @path_to_repo ||= File.join(project.repository_storage_path, "#{disk_path}.git")
- end
-
def update_project_activity
@project.touch(:last_activity_at, :last_repository_updated_at)
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index f2315bb3dbb..5f710961f95 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -50,7 +50,7 @@ class WikiPage
# The Gitlab ProjectWiki instance.
attr_reader :wiki
- # The raw Gollum::Page instance.
+ # The raw Gitlab::Git::WikiPage instance.
attr_reader :page
# The attributes Hash used for storing and validating
@@ -75,7 +75,7 @@ class WikiPage
if @attributes[:slug].present?
@attributes[:slug]
else
- wiki.wiki.preview_page(title, '', format).url_path
+ wiki.wiki.preview_slug(title, format)
end
end
@@ -131,7 +131,7 @@ class WikiPage
def versions
return [] unless persisted?
- @page.versions
+ wiki.wiki.page_versions(@page.path)
end
def commit
@@ -264,8 +264,8 @@ class WikiPage
end
page_title, page_dir = wiki.page_title_and_dir(page_details)
- gollum_wiki = wiki.wiki
- @page = gollum_wiki.paged(page_title, page_dir)
+ gitlab_git_wiki = wiki.wiki
+ @page = gitlab_git_wiki.page(title: page_title, dir: page_dir)
set_attributes
@persisted = errors.blank?
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 2df84e58575..a25882cbb62 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def remove_wip_path
- if can?(current_user, :update_merge_request, merge_request.project)
+ if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project)
remove_wip_project_merge_request_path(project, merge_request)
end
end
diff --git a/app/serializers/container_repositories_serializer.rb b/app/serializers/container_repositories_serializer.rb
new file mode 100644
index 00000000000..56dc70b5687
--- /dev/null
+++ b/app/serializers/container_repositories_serializer.rb
@@ -0,0 +1,3 @@
+class ContainerRepositoriesSerializer < BaseSerializer
+ entity ContainerRepositoryEntity
+end
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
new file mode 100644
index 00000000000..1103cf30a07
--- /dev/null
+++ b/app/serializers/container_repository_entity.rb
@@ -0,0 +1,25 @@
+class ContainerRepositoryEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :path, :location
+
+ expose :tags_path do |repository|
+ project_registry_repository_tags_path(project, repository, format: :json)
+ end
+
+ expose :destroy_path, if: -> (*) { can_destroy? } do |repository|
+ project_container_registry_path(project, repository, format: :json)
+ end
+
+ private
+
+ alias_method :repository, :object
+
+ def project
+ request.project
+ end
+
+ def can_destroy?
+ can?(request.current_user, :update_container_image, project)
+ end
+end
diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb
new file mode 100644
index 00000000000..ec1fc349586
--- /dev/null
+++ b/app/serializers/container_tag_entity.rb
@@ -0,0 +1,23 @@
+class ContainerTagEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name, :location, :revision, :total_size, :created_at
+
+ expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
+ project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json)
+ end
+
+ private
+
+ alias_method :tag, :object
+
+ def project
+ request.project
+ end
+
+ def can_destroy?
+ # TODO: We check permission against @project, not tag,
+ # as tag is no AR object that is attached to project
+ can?(request.current_user, :update_container_image, project)
+ end
+end
diff --git a/app/serializers/container_tags_serializer.rb b/app/serializers/container_tags_serializer.rb
new file mode 100644
index 00000000000..6ff3adff135
--- /dev/null
+++ b/app/serializers/container_tags_serializer.rb
@@ -0,0 +1,17 @@
+class ContainerTagsSerializer < BaseSerializer
+ entity ContainerTagEntity
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ resource = @paginator.paginate(resource) if paginated?
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 7d3c752b8e6..36537c5bd02 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -23,7 +23,6 @@ class MergeRequestEntity < IssuableEntity
expose :closed_event, using: EventEntity
# User entities
- expose :author, using: UserEntity
expose :merge_user, using: UserEntity
# Diff sha's
@@ -31,7 +30,6 @@ class MergeRequestEntity < IssuableEntity
merge_request.diff_head_sha if merge_request.diff_head_commit
end
- expose :merge_commit_sha
expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 6a567487514..5f97d31f610 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -2,13 +2,13 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- - has_any_ci = @merge_requests.any?(&:head_pipeline)
+ - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- if merge_request.head_pipeline
= render_pipeline_status(merge_request.head_pipeline)
- - elsif has_any_ci
+ - elsif has_any_head_pipeline
= icon('blank fw')
%span.merge-request-id
= merge_request.to_reference
diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml
deleted file mode 100644
index a0535edafc3..00000000000
--- a/app/views/projects/registry/repositories/_image.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.container-image.js-toggle-container
- .container-image-head
- = link_to "#", class: "js-toggle-button" do
- = icon('chevron-down', 'aria-hidden': 'true')
- = escape_once(image.path)
-
- = clipboard_button(clipboard_text: "docker pull #{image.location}")
-
- - if can?(current_user, :update_container_image, @project)
- .controls.hidden-xs.pull-right
- = link_to project_container_registry_path(@project, image),
- class: 'btn btn-remove has-tooltip',
- title: 'Remove repository',
- data: { confirm: 'Are you sure?' },
- method: :delete do
- = icon('trash cred', 'aria-hidden': 'true')
-
- .container-image-tags.js-toggle-content.hide
- - if image.has_tags?
- .table-holder
- %table.table.tags
- %thead
- %tr
- %th Tag
- %th Tag ID
- %th Size
- %th Created
- - if can?(current_user, :update_container_image, @project)
- %th
- = render partial: 'tag', collection: image.tags
- - else
- .nothing-here-block No tags in Container Registry for this container image.
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 5661af01302..36ea5e013e4 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,60 +1,49 @@
- page_title "Container Registry"
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
+%section
+ .settings-header
+ %h4
= page_title
%p
- With the Docker Container Registry integrated into GitLab, every project
- can have its own space to store its Docker images.
+ = s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
- Learn more about
- = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
+ = s_('ContainerRegistry|Learn more about')
+ = link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
+ .row.registry-placeholder.prepend-bottom-10
+ .col-lg-12
+ #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
- .col-lg-9
- .panel.panel-default
- .panel-heading
- %h4.panel-title
- How to use the Container Registry
- .panel-body
- %p
- First log in to GitLab&rsquo;s Container Registry using your GitLab username
- and password. If you have
- = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
- you need to use a
- = succeed ':' do
- = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
- %pre
- docker login #{Gitlab.config.registry.host_port}
- %br
- %p
- Once you log in, you&rsquo;re free to create and upload a container image
- using the common
- %code build
- and
- %code push
- commands:
- %pre
- :plain
- docker build -t #{escape_once(@project.container_registry_url)} .
- docker push #{escape_once(@project.container_registry_url)}
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('registry_list')
- %hr
- %h5.prepend-top-default
- Use different image names
- %p.light
- GitLab supports up to 3 levels of image names. The following
- examples of images are valid for your project:
- %pre
- :plain
- #{escape_once(@project.container_registry_url)}:tag
- #{escape_once(@project.container_registry_url)}/optional-image-name:tag
- #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
-
- - if @images.blank?
- %p.settings-message.text-center.append-bottom-default
- No container images stored for this project. Add one by following the
- instructions above.
- - else
- = render partial: 'image', collection: @images
+ .row.prepend-top-10
+ .col-lg-12
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ = s_('ContainerRegistry|How to use the Container Registry')
+ .panel-body
+ %p
+ - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
+ - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
+ = s_('ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token }
+ %pre
+ docker login #{Gitlab.config.registry.host_port}
+ %br
+ %p
+ = s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
+ %pre
+ :plain
+ docker build -t #{escape_once(@project.container_registry_url)} .
+ docker push #{escape_once(@project.container_registry_url)}
+ %hr
+ %h5.prepend-top-default
+ = s_('ContainerRegistry|Use different image names')
+ %p.light
+ = s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
+ %pre
+ :plain
+ #{escape_once(@project.container_registry_url)}:tag
+ #{escape_once(@project.container_registry_url)}/optional-image-name:tag
+ #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index bc1ab5065e4..9ee09262324 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -29,13 +29,13 @@
commit.id, index == 0) do
= truncate_sha(commit.id)
%td
- = commit.author.name
+ = commit.author_name
%td
= commit.message
%td
#{time_ago_with_tooltip(version.authored_date)}
%td
%strong
- = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+ = version.format
= render 'sidebar'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 62c18cc4582..de15fc99eda 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -11,7 +11,7 @@
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author.name}</strong>" }).html_safe
+ = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.commit.authored_date)}
.nav-controls
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 1dfe380db16..4b9af78bc1a 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -7,7 +7,7 @@
= button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do
= icon('caret-down', class: 'toggle-icon')
- %ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
+ %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
diff --git a/changelogs/unreleased/37970-timestamped-ci.yml b/changelogs/unreleased/37970-timestamped-ci.yml
new file mode 100644
index 00000000000..2a4797f069a
--- /dev/null
+++ b/changelogs/unreleased/37970-timestamped-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Strip gitlab-runner section markers in build trace HTML view
+merge_request: 14393
+author:
+type: added
diff --git a/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
new file mode 100644
index 00000000000..579c247c4c2
--- /dev/null
+++ b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bottom spacing for dropdowns that open upwards
+merge_request: 14535
+author:
+type: fixed
diff --git a/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml
new file mode 100644
index 00000000000..419e9295d32
--- /dev/null
+++ b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml
@@ -0,0 +1,5 @@
+---
+title: Use explicit boolean true attribute for show-disabled-button in Vue files
+merge_request: 14672
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml
new file mode 100644
index 00000000000..7f098c8f60c
--- /dev/null
+++ b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml
@@ -0,0 +1,5 @@
+---
+title: Change index on ci_builds to optimize Jobs Controller
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml
new file mode 100644
index 00000000000..13ec113167f
--- /dev/null
+++ b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml
@@ -0,0 +1,5 @@
+---
+title: Bump google-api-client Gem from 0.8.6 to 0.13.6
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/winh-sprintf.yml b/changelogs/unreleased/winh-sprintf.yml
new file mode 100644
index 00000000000..f8ae5932ae4
--- /dev/null
+++ b/changelogs/unreleased/winh-sprintf.yml
@@ -0,0 +1,5 @@
+---
+title: Add basic sprintf implementation to JavaScript
+merge_request: 14506
+author:
+type: other
diff --git a/config/routes/project.rb b/config/routes/project.rb
index b36d13888cd..70d7673250c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -271,7 +271,7 @@ constraints(ProjectUrlConstrainer.new) do
namespace :registry do
resources :repository, only: [] do
- resources :tags, only: [:destroy],
+ resources :tags, only: [:index, :destroy],
constraints: { id: Gitlab::Regex.container_registry_tag_regex }
end
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 3404715fe30..2eb4444916b 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -68,6 +68,7 @@ var config = {
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches',
protected_tags: './protected_tags',
+ registry_list: './registry/index.js',
repo: './repo/index.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
@@ -200,6 +201,7 @@ var config = {
'pdf_viewer',
'pipelines',
'pipelines_details',
+ 'registry_list',
'repo',
'schedule_form',
'schedules_index',
diff --git a/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb
new file mode 100644
index 00000000000..c2cb1df2586
--- /dev/null
+++ b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCiBuildsIndexForJobscontroller < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, [:project_id, :id] unless index_exists? :ci_builds, [:project_id, :id]
+ remove_concurrent_index :ci_builds, :project_id if index_exists? :ci_builds, :project_id
+ end
+
+ def down
+ add_concurrent_index :ci_builds, :project_id unless index_exists? :ci_builds, :project_id
+ remove_concurrent_index :ci_builds, [:project_id, :id] if index_exists? :ci_builds, [:project_id, :id]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d2e45c17426..3428807dd7c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -256,7 +256,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
+ add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index bd0ef39ca62..29c8941a8f7 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -183,13 +183,20 @@ aren't in the message with id `1 pipeline`.
### Interpolation
-- In Ruby/HAML:
+- In Ruby/HAML (see [sprintf]):
```ruby
_("Hello %{name}") % { name: 'Joe' }
```
-- In JavaScript: Not supported at this moment.
+- In JavaScript: Only named parameters are supported (see also [#37992]):
+
+ ```javascript
+ __("Hello %{name}") % { name: 'Joe' }
+ ```
+
+[sprintf]: http://ruby-doc.org/core/Kernel.html#method-i-sprintf
+[#37992]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
### Plurals
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5d45b14f592..7082f31b5b8 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1022,6 +1022,7 @@ module API
expose :cache, using: Cache
expose :credentials, using: Credentials
expose :dependencies, using: Dependency
+ expose :features
end
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 88b17e12576..d8c8deea628 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -73,8 +73,9 @@ module Banzai
return unless node.has_attribute?('href')
begin
+ node['href'] = node['href'].strip
uri = Addressable::URI.parse(node['href'])
- uri.scheme = uri.scheme.strip.downcase if uri.scheme
+ uri.scheme = uri.scheme.downcase if uri.scheme
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index ad78ae244b2..088adbdd267 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -155,7 +155,9 @@ module Gitlab
stream.each_line do |line|
s = StringScanner.new(line)
until s.eos?
- if s.scan(/\e([@-_])(.*?)([@-~])/)
+ if s.scan(/section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/)
+ handle_section(s)
+ elsif s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
break
@@ -183,6 +185,15 @@ module Gitlab
)
end
+ def handle_section(s)
+ action = s[1]
+ timestamp = s[2]
+ section = s[3]
+ line = s.matched()[0...-5] # strips \r\033[0K
+
+ @out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
+ end
+
def handle_sequence(s)
indicator = s[1]
commands = s[2].split ';'
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 22b735c6f7b..a76befe4b52 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -676,7 +676,13 @@ module Gitlab
end
def rm_branch(branch_name, user:)
- OperationService.new(user, self).rm_branch(find_branch(branch_name))
+ gitaly_migrate(:operation_user_delete_branch) do |is_enabled|
+ if is_enabled
+ gitaly_operations_client.user_delete_branch(branch_name, user)
+ else
+ OperationService.new(user, self).rm_branch(find_branch(branch_name))
+ end
+ end
end
def rm_tag(tag_name, user:)
diff --git a/lib/gitlab/git/user.rb b/lib/gitlab/git/user.rb
index cb1af5f3b7c..da74719ae87 100644
--- a/lib/gitlab/git/user.rb
+++ b/lib/gitlab/git/user.rb
@@ -7,6 +7,11 @@ module Gitlab
new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user))
end
+ # TODO support the username field in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/628
+ def self.from_gitaly(gitaly_user)
+ new('', gitaly_user.name, gitaly_user.email, gitaly_user.gl_id)
+ end
+
def initialize(username, name, email, gl_id)
@username = username
@name = name
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
new file mode 100644
index 00000000000..d651c931a38
--- /dev/null
+++ b/lib/gitlab/git/wiki.rb
@@ -0,0 +1,115 @@
+module Gitlab
+ module Git
+ class Wiki
+ DuplicatePageError = Class.new(StandardError)
+
+ CommitDetails = Struct.new(:name, :email, :message) do
+ def to_h
+ { name: name, email: email, message: message }
+ end
+ end
+
+ def self.default_ref
+ 'master'
+ end
+
+ # Initialize with a Gitlab::Git::Repository instance
+ def initialize(repository)
+ @repository = repository
+ end
+
+ def repository_exists?
+ @repository.exists?
+ end
+
+ def write_page(name, format, content, commit_details)
+ assert_type!(format, Symbol)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.write_page(name, format, content, commit_details.to_h)
+
+ nil
+ rescue Gollum::DuplicatePageError => e
+ raise Gitlab::Git::Wiki::DuplicatePageError, e.message
+ end
+
+ def delete_page(page_path, commit_details)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
+ nil
+ end
+
+ def update_page(page_path, title, format, content, commit_details)
+ assert_type!(format, Symbol)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
+ nil
+ end
+
+ def pages
+ gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
+ end
+
+ def page(title:, version: nil, dir: nil)
+ if version
+ version = Gitlab::Git::Commit.find(@repository, version).id
+ end
+
+ gollum_page = gollum_wiki.page(title, version, dir)
+ return unless gollum_page
+
+ new_page(gollum_page)
+ end
+
+ def file(name, version)
+ version ||= self.class.default_ref
+ gollum_file = gollum_wiki.file(name, version)
+ return unless gollum_file
+
+ Gitlab::Git::WikiFile.new(gollum_file)
+ end
+
+ def page_versions(page_path)
+ current_page = gollum_page_by_path(page_path)
+ current_page.versions.map do |gollum_git_commit|
+ gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id)
+ new_version(gollum_page, gollum_git_commit.id)
+ end
+ end
+
+ def preview_slug(title, format)
+ gollum_wiki.preview_page(title, '', format).url_path
+ end
+
+ private
+
+ def gollum_wiki
+ @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ end
+
+ def gollum_page_by_path(page_path)
+ page_name = Gollum::Page.canonicalize_filename(page_path)
+ page_dir = File.split(page_path).first
+
+ gollum_wiki.paged(page_name, page_dir)
+ end
+
+ def new_page(gollum_page)
+ Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id))
+ end
+
+ def new_version(gollum_page, commit_id)
+ commit = Gitlab::Git::Commit.find(@repository, commit_id)
+ Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format)
+ end
+
+ def assert_type!(object, klass)
+ unless object.is_a?(klass)
+ raise ArgumentError, "expected a #{klass}, got #{object.inspect}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb
new file mode 100644
index 00000000000..527f2a44dea
--- /dev/null
+++ b/lib/gitlab/git/wiki_file.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Git
+ class WikiFile
+ attr_reader :mime_type, :raw_data, :name
+
+ # This class is meant to be serializable so that it can be constructed
+ # by Gitaly and sent over the network to GitLab.
+ #
+ # Because Gollum::File is not serializable we must get all the data from
+ # 'gollum_file' during initialization, and NOT store it in an instance
+ # variable.
+ def initialize(gollum_file)
+ @mime_type = gollum_file.mime_type
+ @raw_data = gollum_file.raw_data
+ @name = gollum_file.name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb
new file mode 100644
index 00000000000..a06bac4414f
--- /dev/null
+++ b/lib/gitlab/git/wiki_page.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module Git
+ class WikiPage
+ attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical
+
+ # This class is meant to be serializable so that it can be constructed
+ # by Gitaly and sent over the network to GitLab.
+ #
+ # Because Gollum::Page is not serializable we must get all the data from
+ # 'gollum_page' during initialization, and NOT store it in an instance
+ # variable.
+ #
+ # Note that 'version' is a WikiPageVersion instance which it itself
+ # serializable. That means it's OK to store 'version' in an instance
+ # variable.
+ def initialize(gollum_page, version)
+ @url_path = gollum_page.url_path
+ @title = gollum_page.title
+ @format = gollum_page.format
+ @path = gollum_page.path
+ @raw_data = gollum_page.raw_data
+ @name = gollum_page.name
+ @historical = gollum_page.historical?
+
+ @version = version
+ end
+
+ def historical?
+ @historical
+ end
+
+ def text_data
+ return @text_data if defined?(@text_data)
+
+ @text_data = @raw_data && Gitlab::EncodingHelper.encode!(@raw_data.dup)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/wiki_page_version.rb b/lib/gitlab/git/wiki_page_version.rb
new file mode 100644
index 00000000000..55f1afedcab
--- /dev/null
+++ b/lib/gitlab/git/wiki_page_version.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Git
+ class WikiPageVersion
+ attr_reader :commit, :format
+
+ # This class is meant to be serializable so that it can be constructed
+ # by Gitaly and sent over the network to GitLab.
+ #
+ # Both 'commit' (a Gitlab::Git::Commit) and 'format' (a string) are
+ # serializable.
+ def initialize(commit, format)
+ @commit = commit
+ @format = format
+ end
+
+ delegate :message, :sha, :id, :author_name, :authored_date, to: :commit
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 46bd5c18603..81ddaf13e10 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -60,6 +60,20 @@ module Gitlab
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit)
end
+
+ def user_delete_branch(branch_name, user)
+ request = Gitaly::UserDeleteBranchRequest.new(
+ repository: @gitaly_repo,
+ branch_name: GitalyClient.encode(branch_name),
+ user: Util.gitaly_user(user)
+ )
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request)
+
+ if pre_receive_error = response.pre_receive_error.presence
+ raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
+ end
+ end
end
end
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 4e1ec1402ea..1caa791c1be 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,9 @@
module Gitlab
class UrlSanitizer
+ ALLOWED_SCHEMES = %w[http https ssh git].freeze
+
def self.sanitize(content)
- regexp = URI::Parser.new.make_regexp(%w(http https ssh git))
+ regexp = URI::Parser.new.make_regexp(ALLOWED_SCHEMES)
content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError
@@ -11,9 +13,9 @@ module Gitlab
def self.valid?(url)
return false unless url.present?
- Addressable::URI.parse(url.strip)
+ uri = Addressable::URI.parse(url.strip)
- true
+ ALLOWED_SCHEMES.include?(uri.scheme)
rescue Addressable::URI::InvalidURIError
false
end
diff --git a/qa/Gemfile b/qa/Gemfile
index 5d089a45934..ff29824529f 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -1,5 +1,6 @@
source 'https://rubygems.org'
+gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'capybara', '~> 2.12.1'
gem 'capybara-screenshot', '~> 1.0.14'
gem 'rake', '~> 12.0.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 4dd71aa5010..95aeef10752 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -3,6 +3,7 @@ GEM
specs:
addressable (2.5.0)
public_suffix (~> 2.0, >= 2.0.2)
+ byebug (9.0.6)
capybara (2.12.1)
addressable
mime-types (>= 1.16)
@@ -13,22 +14,27 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- capybara-webkit (1.12.0)
- capybara (>= 2.3.0, < 2.13.0)
- json
childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11)
+ coderay (1.1.1)
diff-lcs (1.3)
ffi (1.9.18)
- json (2.0.3)
launchy (2.4.3)
addressable (~> 2.3)
+ method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0)
+ pry (0.10.4)
+ coderay (~> 1.1.0)
+ method_source (~> 0.8.1)
+ slop (~> 3.4)
+ pry-byebug (3.4.2)
+ byebug (~> 9.0)
+ pry (~> 0.10)
public_suffix (2.0.5)
rack (2.0.1)
rack-test (0.6.3)
@@ -52,6 +58,7 @@ GEM
childprocess (~> 0.5)
rubyzip (~> 1.0)
websocket (~> 1.0)
+ slop (3.6.0)
websocket (1.2.4)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -62,7 +69,7 @@ PLATFORMS
DEPENDENCIES
capybara (~> 2.12.1)
capybara-screenshot (~> 1.0.14)
- capybara-webkit (~> 1.12.0)
+ pry-byebug (~> 3.4.1)
rake (~> 12.0.0)
rspec (~> 3.5)
selenium-webdriver (~> 2.53)
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index f4619042e34..baa06b1c75e 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -4,8 +4,6 @@ module QA
class Menu < Page::Base
def go_to_license
link = find_link 'License'
- # Click space to scroll this link into the view
- link.send_keys(:space)
link.click
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 56a270d8fcc..68d9597c4d2 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -5,8 +5,8 @@ module QA
def choose_repository_clone_http
find('#clone-dropdown').click
- page.within('#clone-dropdown') do
- find('span', text: 'HTTP').click
+ page.within('.clone-options-dropdown') do
+ click_link('HTTP')
end
end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index 4dfdd6cd93c..79c681168cc 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -43,8 +43,7 @@ module QA
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => {
- 'binary' => '/usr/bin/google-chrome-stable',
- 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024]
+ 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
}
)
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 629c131aee6..e46d1995498 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do
expect(response).to match_response_schema('entities/merge_request')
end
end
-
- context 'number of queries', :request_store do
- it 'verifies number of queries' do
- # pre-create objects
- merge_request
-
- recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
-
- expect(recorded.count).to be_within(5).of(30)
- expect(recorded.cached_count).to eq(0)
- end
- end
end
describe "as diff" do
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index 2805968dcd9..5d9d5351687 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do
expect { go_to_index }.to change { ContainerRepository.all.count }.by(1)
expect(ContainerRepository.first).to be_root_repository
end
+
+ it 'json has a list of projects' do
+ go_to_index(format: :json)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('registry/repositories')
+ end
end
context 'when there are no tags for this repository' do
@@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do
it 'does not ensure root container repository' do
expect { go_to_index }.not_to change { ContainerRepository.all.count }
end
+
+ it 'responds with json if asked' do
+ go_to_index(format: :json)
+
+ expect(response).to have_http_status(:ok)
+ expect(json_response).to be_kind_of(Array)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE destroy' do
+ context 'when root container repository exists' do
+ let!(:repository) do
+ create(:container_repository, :root, project: project)
+ end
+
+ before do
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
+
+ it 'deletes a repository' do
+ expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1)
+
+ expect(response).to have_http_status(:no_content)
end
end
end
@@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do
end
end
- def go_to_index
+ def go_to_index(format: :html)
get :index, namespace_id: project.namespace,
- project_id: project
+ project_id: project,
+ format: format
+ end
+
+ def delete_repository(repository)
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: repository,
+ format: :json
end
end
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index f4af3587d23..bb702ebeb23 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do
let(:user) { create(:user) }
let(:project) { create(:project, :private) }
+ let(:repository) do
+ create(:container_repository, name: 'image', project: project)
+ end
+
before do
sign_in(user)
stub_container_registry_config(enabled: true)
end
- context 'when user has access to registry' do
+ describe 'GET index' do
+ let(:tags) do
+ Array.new(40) { |i| "tag#{i}" }
+ end
+
before do
- project.add_developer(user)
+ stub_container_registry_tags(repository: /image/, tags: tags)
end
- describe 'POST destroy' do
+ context 'when user can control the registry' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'receive a list of tags' do
+ get_tags
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ context 'when user can read the registry' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'receive a list of tags' do
+ get_tags
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ context 'when user does not have access to registry' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does not receive a list of tags' do
+ get_tags
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ private
+
+ def get_tags
+ get :index, namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ format: :json
+ end
+ end
+
+ describe 'POST destroy' do
+ context 'when user has access to registry' do
+ before do
+ project.add_developer(user)
+ end
+
context 'when there is matching tag present' do
before do
- stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.])
- end
-
- let(:repository) do
- create(:container_repository, name: 'image', project: project)
+ stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
end
it 'makes it possible to delete regular tag' do
@@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do
end
end
end
- end
- def destroy_tag(name)
- post :destroy, namespace_id: project.namespace,
- project_id: project,
- repository_id: repository,
- id: name
+ private
+
+ def destroy_tag(name)
+ post :destroy, namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ id: name,
+ format: :json
+ end
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index ae39ba4da6b..45213dc6995 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Container Registry" do
+describe "Container Registry", js: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -41,16 +41,19 @@ describe "Container Registry" do
expect_any_instance_of(ContainerRepository)
.to receive(:delete_tags!).and_return(true)
- click_on 'Remove repository'
+ click_on(class: 'js-remove-repo')
end
scenario 'user removes a specific tag from container repository' do
visit_container_registry
+ find('.js-toggle-repo').trigger('click')
+ wait_for_requests
+
expect_any_instance_of(ContainerRegistry::Tag)
.to receive(:delete).and_return(true)
- click_on 'Remove tag'
+ click_on(class: 'js-delete-registry')
end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 49ba2969ef0..470391dc66b 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -83,7 +83,7 @@ describe 'User views a wiki page' do
it 'shows a file stored in a page' do
file = Gollum::File.new(project.wiki)
- allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master', true).and_return(file)
+ allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master').and_return(file)
allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg')
expect(page).to have_xpath('//img[@data-src="image.jpg"]')
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 0796d9b8af9..30b4e56bc98 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -93,7 +93,7 @@
"merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
- "remove_wip_path": { "type": "string" },
+ "remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] },
"merge_ongoing": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/registry/repositories.json b/spec/fixtures/api/schemas/registry/repositories.json
new file mode 100644
index 00000000000..4978bd89cda
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/repositories.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "repository.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json
new file mode 100644
index 00000000000..4175642eb00
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/repository.json
@@ -0,0 +1,27 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "path",
+ "location",
+ "tags_path"
+ ],
+ "properties" : {
+ "id": {
+ "type": "integer"
+ },
+ "path": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "tags_path": {
+ "type": "string"
+ },
+ "destroy_path": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json
new file mode 100644
index 00000000000..5bc307e0e64
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/tag.json
@@ -0,0 +1,28 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "location"
+ ],
+ "properties" : {
+ "name": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "revision": {
+ "type": "string"
+ },
+ "total_size": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "destroy_path": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/registry/tags.json b/spec/fixtures/api/schemas/registry/tags.json
new file mode 100644
index 00000000000..c72f957459a
--- /dev/null
+++ b/spec/fixtures/api/schemas/registry/tags.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "tag.json"
+ }
+}
diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/helpers/vuex_action_helper.js
index 2d386fe1da5..2d386fe1da5 100644
--- a/spec/javascripts/notes/stores/helpers.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/javascripts/locale/sprintf_spec.js
new file mode 100644
index 00000000000..52e903b819f
--- /dev/null
+++ b/spec/javascripts/locale/sprintf_spec.js
@@ -0,0 +1,74 @@
+import sprintf from '~/locale/sprintf';
+
+describe('locale', () => {
+ describe('sprintf', () => {
+ it('does not modify string without parameters', () => {
+ const input = 'No parameters';
+
+ const output = sprintf(input);
+
+ expect(output).toBe(input);
+ });
+
+ it('ignores extraneous parameters', () => {
+ const input = 'No parameters';
+
+ const output = sprintf(input, { ignore: 'this' });
+
+ expect(output).toBe(input);
+ });
+
+ it('ignores extraneous placeholders', () => {
+ const input = 'No %{parameters}';
+
+ const output = sprintf(input);
+
+ expect(output).toBe(input);
+ });
+
+ it('replaces parameters', () => {
+ const input = '%{name} has %{count} parameters';
+ const parameters = {
+ name: 'this',
+ count: 2,
+ };
+
+ const output = sprintf(input, parameters);
+
+ expect(output).toBe('this has 2 parameters');
+ });
+
+ it('replaces multiple occurrences', () => {
+ const input = 'to %{verb} or not to %{verb}';
+ const parameters = {
+ verb: 'be',
+ };
+
+ const output = sprintf(input, parameters);
+
+ expect(output).toBe('to be or not to be');
+ });
+
+ it('escapes parameters', () => {
+ const input = 'contains %{userContent}';
+ const parameters = {
+ userContent: '<script>alert("malicious!")</script>',
+ };
+
+ const output = sprintf(input, parameters);
+
+ expect(output).toBe('contains &lt;script&gt;alert(&quot;malicious!&quot;)&lt;/script&gt;');
+ });
+
+ it('does not escape parameters for escapeParameters = false', () => {
+ const input = 'contains %{safeContent}';
+ const parameters = {
+ safeContent: '<strong>bold attempt</strong>',
+ };
+
+ const output = sprintf(input, parameters, false);
+
+ expect(output).toBe('contains <strong>bold attempt</strong>');
+ });
+ });
+});
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 2b2219dcf0c..3d1ca870ca4 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -1,5 +1,5 @@
import * as actions from '~/notes/stores/actions';
-import testAction from './helpers';
+import testAction from '../../helpers/vuex_action_helper';
import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
new file mode 100644
index 00000000000..43e7d9e1224
--- /dev/null
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import registry from '~/registry/components/app.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { reposServerResponse } from '../mock_data';
+
+describe('Registry List', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(registry);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ vm = mountComponent(Component, { endpoint: 'foo' });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render a list of repos', (done) => {
+ setTimeout(() => {
+ expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.container-image').length,
+ ).toEqual(reposServerResponse.length);
+ done();
+ });
+ }, 0);
+ });
+
+ describe('delete repository', () => {
+ it('should be possible to delete a repo', (done) => {
+ setTimeout(() => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
+ done();
+ });
+ }, 0);
+ });
+ });
+
+ describe('toggle repository', () => {
+ it('should open the container', (done) => {
+ setTimeout(() => {
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up');
+ done();
+ });
+ });
+ }, 0);
+ });
+ });
+ });
+
+ describe('without data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ vm = mountComponent(Component, { endpoint: 'foo' });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render empty message', (done) => {
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('p').textContent.trim(),
+ ).toEqual('No container images stored for this project. Add one by following the instructions above.');
+ done();
+ }, 0);
+ });
+ });
+
+ describe('while loading data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ vm = mountComponent(Component, { endpoint: 'foo' });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('should render a loading spinner', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js
new file mode 100644
index 00000000000..5891921318a
--- /dev/null
+++ b/spec/javascripts/registry/components/collapsible_container_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import collapsibleComponent from '~/registry/components/collapsible_container.vue';
+import store from '~/registry/stores';
+import { repoPropsData } from '../mock_data';
+
+describe('collapsible registry container', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(collapsibleComponent);
+ vm = new Component({
+ store,
+ propsData: {
+ repo: repoPropsData,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggle', () => {
+ it('should be closed by default', () => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
+ expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
+ });
+
+ it('should be open when user clicks on closed repo', (done) => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBeDefined();
+ expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up');
+ done();
+ });
+ });
+
+ it('should be closed when the user clicks on an opened repo', (done) => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.js-toggle-repo').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
+ expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('delete repo', () => {
+ it('should be possible to delete a repo', () => {
+ expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js
new file mode 100644
index 00000000000..6aa61afc445
--- /dev/null
+++ b/spec/javascripts/registry/components/table_registry_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import tableRegistry from '~/registry/components/table_registry.vue';
+import store from '~/registry/stores';
+import { repoPropsData } from '../mock_data';
+
+describe('table registry', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(tableRegistry);
+ vm = new Component({
+ store,
+ propsData: {
+ repo: repoPropsData,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a table with the registry list', () => {
+ expect(
+ vm.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(repoPropsData.list.length);
+ });
+
+ it('should render registry tag', () => {
+ const textRendered = vm.$el.querySelector('.table tbody tr').textContent.trim().replace(/\s\s+/g, ' ');
+ expect(textRendered).toContain(repoPropsData.list[0].tag);
+ expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
+ expect(textRendered).toContain(repoPropsData.list[0].layers);
+ expect(textRendered).toContain(repoPropsData.list[0].size);
+ });
+
+ it('should be possible to delete a registry', () => {
+ expect(
+ vm.$el.querySelector('.table tbody tr .js-delete-registry'),
+ ).toBeDefined();
+ });
+
+ describe('pagination', () => {
+ it('should be possible to change the page', () => {
+ expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/registry/getters_spec.js b/spec/javascripts/registry/getters_spec.js
new file mode 100644
index 00000000000..3d989541881
--- /dev/null
+++ b/spec/javascripts/registry/getters_spec.js
@@ -0,0 +1,43 @@
+import * as getters from '~/registry/stores/getters';
+
+describe('Getters Registry Store', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ isLoading: false,
+ endpoint: '/root/empty-project/container_registry.json',
+ repos: [{
+ canDelete: true,
+ destroyPath: 'bar',
+ id: '134',
+ isLoading: false,
+ list: [],
+ location: 'foo',
+ name: 'gitlab-org/omnibus-gitlab/foo',
+ tagsPath: 'foo',
+ }, {
+ canDelete: true,
+ destroyPath: 'bar',
+ id: '123',
+ isLoading: false,
+ list: [],
+ location: 'foo',
+ name: 'gitlab-org/omnibus-gitlab',
+ tagsPath: 'foo',
+ }],
+ };
+ });
+
+ describe('isLoading', () => {
+ it('should return the isLoading property', () => {
+ expect(getters.isLoading(state)).toEqual(state.isLoading);
+ });
+ });
+
+ describe('repos', () => {
+ it('should return the repos', () => {
+ expect(getters.repos(state)).toEqual(state.repos);
+ });
+ });
+});
diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js
new file mode 100644
index 00000000000..18600d00bff
--- /dev/null
+++ b/spec/javascripts/registry/mock_data.js
@@ -0,0 +1,122 @@
+export const defaultState = {
+ isLoading: false,
+ endpoint: '',
+ repos: [],
+};
+
+export const reposServerResponse = [
+ {
+ destroy_path: 'path',
+ id: '123',
+ location: 'location',
+ path: 'foo',
+ tags_path: 'tags_path',
+ },
+ {
+ destroy_path: 'path_',
+ id: '456',
+ location: 'location_',
+ path: 'bar',
+ tags_path: 'tags_path_',
+ },
+];
+
+export const registryServerResponse = [
+ {
+ name: 'centos7',
+ short_revision: 'b118ab5b0',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ size: 679,
+ layers: 19,
+ location: 'location',
+ created_at: 1505828744434,
+ destroy_path: 'path_',
+ },
+ {
+ name: 'centos6',
+ short_revision: 'b118ab5b0',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ size: 679,
+ layers: 19,
+ location: 'location',
+ created_at: 1505828744434,
+ }];
+
+export const parsedReposServerResponse = [
+ {
+ canDelete: true,
+ destroyPath: reposServerResponse[0].destroy_path,
+ id: reposServerResponse[0].id,
+ isLoading: false,
+ list: [],
+ location: reposServerResponse[0].location,
+ name: reposServerResponse[0].path,
+ tagsPath: reposServerResponse[0].tags_path,
+ },
+ {
+ canDelete: true,
+ destroyPath: reposServerResponse[1].destroy_path,
+ id: reposServerResponse[1].id,
+ isLoading: false,
+ list: [],
+ location: reposServerResponse[1].location,
+ name: reposServerResponse[1].path,
+ tagsPath: reposServerResponse[1].tags_path,
+ },
+];
+
+export const parsedRegistryServerResponse = [
+ {
+ tag: registryServerResponse[0].name,
+ revision: registryServerResponse[0].revision,
+ shortRevision: registryServerResponse[0].short_revision,
+ size: registryServerResponse[0].size,
+ layers: registryServerResponse[0].layers,
+ location: registryServerResponse[0].location,
+ createdAt: registryServerResponse[0].created_at,
+ destroyPath: registryServerResponse[0].destroy_path,
+ canDelete: true,
+ },
+ {
+ tag: registryServerResponse[1].name,
+ revision: registryServerResponse[1].revision,
+ shortRevision: registryServerResponse[1].short_revision,
+ size: registryServerResponse[1].size,
+ layers: registryServerResponse[1].layers,
+ location: registryServerResponse[1].location,
+ createdAt: registryServerResponse[1].created_at,
+ destroyPath: registryServerResponse[1].destroy_path,
+ canDelete: false,
+ },
+];
+
+export const repoPropsData = {
+ canDelete: true,
+ destroyPath: 'path',
+ id: '123',
+ isLoading: false,
+ list: [
+ {
+ tag: 'centos6',
+ revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
+ shortRevision: 'b118ab5b0',
+ size: 19,
+ layers: 10,
+ location: 'location',
+ createdAt: 1505828744434,
+ destroyPath: 'path',
+ canDelete: true,
+ },
+ ],
+ location: 'location',
+ name: 'foo',
+ tagsPath: 'path',
+ pagination: {
+ perPage: 5,
+ page: 1,
+ total: 13,
+ totalPages: 1,
+ nextPage: null,
+ previousPage: null,
+ },
+};
diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js
new file mode 100644
index 00000000000..3c9da4f107b
--- /dev/null
+++ b/spec/javascripts/registry/stores/actions_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import _ from 'underscore';
+import * as actions from '~/registry/stores/actions';
+import * as types from '~/registry/stores/mutation_types';
+import testAction from '../../helpers/vuex_action_helper';
+import {
+ defaultState,
+ reposServerResponse,
+ registryServerResponse,
+ parsedReposServerResponse,
+} from '../mock_data';
+
+Vue.use(VueResource);
+
+describe('Actions Registry Store', () => {
+ let interceptor;
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = defaultState;
+ });
+
+ describe('server requests', () => {
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ describe('fetchRepos', () => {
+ beforeEach(() => {
+ interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }));
+ };
+
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ it('should set receveived repos', (done) => {
+ testAction(actions.fetchRepos, null, mockedState, [
+ { type: types.TOGGLE_MAIN_LOADING },
+ { type: types.SET_REPOS_LIST, payload: reposServerResponse },
+ ], done);
+ });
+ });
+
+ describe('fetchList', () => {
+ beforeEach(() => {
+ interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(registryServerResponse), {
+ status: 200,
+ }));
+ };
+
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ it('should set received list', (done) => {
+ mockedState.repos = parsedReposServerResponse;
+
+ testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING },
+ { type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
+ ], done);
+ });
+ });
+ });
+
+ describe('setMainEndpoint', () => {
+ it('should commit set main endpoint', (done) => {
+ testAction(actions.setMainEndpoint, 'endpoint', mockedState, [
+ { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' },
+ ], done);
+ });
+ });
+
+ describe('toggleLoading', () => {
+ it('should commit toggle main loading', (done) => {
+ testAction(actions.toggleLoading, null, mockedState, [
+ { type: types.TOGGLE_MAIN_LOADING },
+ ], done);
+ });
+ });
+});
diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js
new file mode 100644
index 00000000000..2e4c0659daa
--- /dev/null
+++ b/spec/javascripts/registry/stores/mutations_spec.js
@@ -0,0 +1,81 @@
+import mutations from '~/registry/stores/mutations';
+import * as types from '~/registry/stores/mutation_types';
+import {
+ defaultState,
+ reposServerResponse,
+ registryServerResponse,
+ parsedReposServerResponse,
+ parsedRegistryServerResponse,
+} from '../mock_data';
+
+describe('Mutations Registry Store', () => {
+ let mockState;
+ beforeEach(() => {
+ mockState = defaultState;
+ });
+
+ describe('SET_MAIN_ENDPOINT', () => {
+ it('should set the main endpoint', () => {
+ const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
+ mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
+ expect(mockState).toEqual(expectedState);
+ });
+ });
+
+ describe('SET_REPOS_LIST', () => {
+ it('should set a parsed repository list', () => {
+ mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
+ expect(mockState.repos).toEqual(parsedReposServerResponse);
+ });
+ });
+
+ describe('TOGGLE_MAIN_LOADING', () => {
+ it('should set a parsed repository list', () => {
+ mutations[types.TOGGLE_MAIN_LOADING](mockState);
+ expect(mockState.isLoading).toEqual(true);
+ });
+ });
+
+ describe('SET_REGISTRY_LIST', () => {
+ it('should set a list of registries in a specific repository', () => {
+ mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
+ mutations[types.SET_REGISTRY_LIST](mockState, {
+ repo: mockState.repos[0],
+ resp: registryServerResponse,
+ headers: {
+ 'x-per-page': 2,
+ 'x-page': 1,
+ 'x-total': 10,
+ },
+ });
+
+ expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse);
+ expect(mockState.repos[0].pagination).toEqual({
+ perPage: 2,
+ page: 1,
+ total: 10,
+ totalPages: NaN,
+ nextPage: NaN,
+ previousPage: NaN,
+ });
+ });
+ });
+
+ describe('TOGGLE_REGISTRY_LIST_LOADING', () => {
+ it('should toggle isLoading property for a specific repository', () => {
+ mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
+ mutations[types.SET_REGISTRY_LIST](mockState, {
+ repo: mockState.repos[0],
+ resp: registryServerResponse,
+ headers: {
+ 'x-per-page': 2,
+ 'x-page': 1,
+ 'x-total': 10,
+ },
+ });
+
+ mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]);
+ expect(mockState.repos[0].isLoading).toEqual(true);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index e6645985ba4..33540eab5d6 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -195,6 +195,32 @@ describe Gitlab::Ci::Ansi2html do
end
end
+ context "with section markers" do
+ let(:section_name) { 'test_section' }
+ let(:section_start_time) { Time.new(2017, 9, 20).utc }
+ let(:section_duration) { 3.seconds }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_start_html) do
+ '<div class="hidden" data-action="start"'\
+ " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{section_name}\">"\
+ "#{section_start[0...-5]}</div>"
+ end
+ let(:section_end_html) do
+ '<div class="hidden" data-action="end"'\
+ " data-timestamp=\"#{section_end_time.to_i}\" data-section=\"#{section_name}\">"\
+ "#{section_end[0...-5]}</div>"
+ end
+
+ it "prints light red" do
+ text = "#{section_start}\e[91mHello\e[0m\n#{section_end}"
+ html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}}
+
+ expect(convert_html(text)).to eq(html)
+ end
+ end
+
describe "truncates" do
let(:text) { "Hello World" }
let(:stream) { StringIO.new(text) }
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index a0482e30a33..f405b2f2684 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1444,6 +1444,34 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#rm_branch' do
+ shared_examples "user deleting a branch" do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw }
+ let(:user) { create(:user) }
+ let(:branch_name) { "to-be-deleted-soon" }
+
+ before do
+ project.team << [user, :developer]
+ repository.create_branch(branch_name)
+ end
+
+ it "removes the branch from the repo" do
+ repository.rm_branch(branch_name, user: user)
+
+ expect(repository.rugged.branches[branch_name]).to be_nil
+ end
+ end
+
+ context "when Gitaly user_delete_branch is enabled" do
+ it_behaves_like "user deleting a branch"
+ end
+
+ context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do
+ it_behaves_like "user deleting a branch"
+ end
+ end
+
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
diff --git a/spec/lib/gitlab/git/user_spec.rb b/spec/lib/gitlab/git/user_spec.rb
index ab64b041187..31d5f59a562 100644
--- a/spec/lib/gitlab/git/user_spec.rb
+++ b/spec/lib/gitlab/git/user_spec.rb
@@ -8,6 +8,20 @@ describe Gitlab::Git::User do
subject { described_class.new(username, name, email, gl_id) }
+ describe '.from_gitaly' do
+ let(:gitaly_user) { Gitaly::User.new(name: name, email: email, gl_id: gl_id) }
+ subject { described_class.from_gitaly(gitaly_user) }
+
+ it { expect(subject).to eq(described_class.new('', name, email, gl_id)) }
+ end
+
+ describe '.from_gitlab' do
+ let(:user) { build(:user) }
+ subject { described_class.from_gitlab(user) }
+
+ it { expect(subject).to eq(described_class.new(user.username, user.name, user.email, 'user-')) }
+ end
+
describe '#==' do
def eq_other(username, name, email, gl_id)
eq(described_class.new(username, name, email, gl_id))
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 769b14687ac..7bd6a7fa842 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do
let(:project) { create(:project) }
let(:repository) { project.repository.raw }
let(:client) { described_class.new(repository) }
+ let(:user) { create(:user) }
+ let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
describe '#user_create_branch' do
- let(:user) { create(:user) }
- let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
let(:branch_name) { 'new' }
let(:start_point) { 'master' }
let(:request) do
@@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_delete_branch' do
+ let(:branch_name) { 'my-branch' }
+ let(:request) do
+ Gitaly::UserDeleteBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch_name: branch_name,
+ user: gitaly_user
+ )
+ end
+ let(:response) { Gitaly::UserDeleteBranchResponse.new }
+
+ subject { client.user_delete_branch(branch_name, user) }
+
+ it 'sends a user_delete_branch message' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_delete_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ subject
+ end
+
+ context "when pre_receive_error is present" do
+ let(:response) do
+ Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_delete_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::HooksService::PreReceiveError, "something failed")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 59c28431e1e..fc8991fd31f 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -39,7 +39,8 @@ describe Gitlab::UrlSanitizer do
false | nil
false | ''
false | '123://invalid:url'
- true | 'valid@project:url.git'
+ false | 'valid@project:url.git'
+ false | 'valid:pass@project:url.git'
true | 'ssh://example.com'
true | 'ssh://:@example.com'
true | 'ssh://foo@example.com'
@@ -81,24 +82,6 @@ describe Gitlab::UrlSanitizer do
describe '#credentials' do
context 'credentials in hash' do
- where(:input, :output) do
- { user: 'foo', password: 'bar' } | { user: 'foo', password: 'bar' }
- { user: 'foo', password: '' } | { user: 'foo', password: nil }
- { user: 'foo', password: nil } | { user: 'foo', password: nil }
- { user: '', password: 'bar' } | { user: nil, password: 'bar' }
- { user: '', password: '' } | { user: nil, password: nil }
- { user: '', password: nil } | { user: nil, password: nil }
- { user: nil, password: 'bar' } | { user: nil, password: 'bar' }
- { user: nil, password: '' } | { user: nil, password: nil }
- { user: nil, password: nil } | { user: nil, password: nil }
- end
-
- with_them do
- subject { described_class.new('user@example.com:path.git', credentials: input).credentials }
-
- it { is_expected.to eq(output) }
- end
-
it 'overrides URL-provided credentials' do
sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' })
@@ -116,10 +99,6 @@ describe Gitlab::UrlSanitizer do
'http://@example.com' | { user: nil, password: nil }
'http://example.com' | { user: nil, password: nil }
- # Credentials from SCP-style URLs are not supported at present
- 'foo@example.com:path' | { user: nil, password: nil }
- 'foo:bar@example.com:path' | { user: nil, password: nil }
-
# Other invalid URLs
nil | { user: nil, password: nil }
'' | { user: nil, password: nil }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d80d5657c42..188a0a98ec3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -791,6 +791,49 @@ describe MergeRequest do
end
end
+ describe '#has_ci?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ context 'has ci' do
+ it 'returns true if MR has head_pipeline_id and commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
+ allow(merge_request).to receive(:head_pipeline_id) { double }
+ allow(merge_request).to receive(:has_no_commits?) { false }
+
+ expect(merge_request.has_ci?).to be(true)
+ end
+
+ it 'returns true if MR has any pipeline and commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
+ allow(merge_request).to receive(:head_pipeline_id) { nil }
+ allow(merge_request).to receive(:has_no_commits?) { false }
+ allow(merge_request).to receive(:all_pipelines) { [double] }
+
+ expect(merge_request.has_ci?).to be(true)
+ end
+
+ it 'returns true if MR has CI service and commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double }
+ allow(merge_request).to receive(:head_pipeline_id) { nil }
+ allow(merge_request).to receive(:has_no_commits?) { false }
+ allow(merge_request).to receive(:all_pipelines) { [] }
+
+ expect(merge_request.has_ci?).to be(true)
+ end
+ end
+
+ context 'has no ci' do
+ it 'returns false if MR has no CI service nor pipeline, and no commits' do
+ allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
+ allow(merge_request).to receive(:head_pipeline_id) { nil }
+ allow(merge_request).to receive(:all_pipelines) { [] }
+ allow(merge_request).to receive(:has_no_commits?) { true }
+
+ expect(merge_request.has_ci?).to be(false)
+ end
+ end
+ end
+
describe '#all_pipelines' do
shared_examples 'returning pipelines with proper ordering' do
let!(:all_pipelines) do
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 953df7746eb..78fb2df884a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -6,13 +6,10 @@ describe ProjectWiki do
let(:user) { project.owner }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project_wiki) { described_class.new(project, user) }
+ let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo') }
subject { project_wiki }
- before do
- project_wiki.wiki
- end
-
describe "#path_with_namespace" do
it "returns the project path with namespace with the .wiki extension" do
expect(subject.path_with_namespace).to eq(project.full_path + '.wiki')
@@ -61,8 +58,8 @@ describe ProjectWiki do
end
describe "#wiki" do
- it "contains a Gollum::Wiki instance" do
- expect(subject.wiki).to be_a Gollum::Wiki
+ it "contains a Gitlab::Git::Wiki instance" do
+ expect(subject.wiki).to be_a Gitlab::Git::Wiki
end
it "creates a new wiki repo if one does not yet exist" do
@@ -70,20 +67,18 @@ describe ProjectWiki do
end
it "raises CouldNotCreateWikiError if it can't create the wiki repository" do
- allow(project_wiki).to receive(:init_repo).and_return(false)
- expect { project_wiki.send(:create_repo!) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
+ # Create a fresh project which will not have a wiki
+ project_wiki = described_class.new(create(:project), user)
+ gitlab_shell = double(:gitlab_shell)
+ allow(gitlab_shell).to receive(:add_repository)
+ allow(project_wiki).to receive(:gitlab_shell).and_return(gitlab_shell)
+
+ expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
end
end
describe "#empty?" do
context "when the wiki repository is empty" do
- before do
- allow_any_instance_of(Gitlab::Shell).to receive(:add_repository) do
- create_temp_repo("#{Rails.root}/tmp/test-git-base-path/non-existant.wiki.git")
- end
- allow(project).to receive(:full_path).and_return("non-existant")
- end
-
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_truthy }
@@ -154,13 +149,13 @@ describe ProjectWiki do
before do
file = Gollum::File.new(subject.wiki)
allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('image.jpg', 'master', true)
+ .to receive(:file).with('image.jpg', 'master')
.and_return(file)
allow_any_instance_of(Gollum::File)
.to receive(:mime_type)
.and_return('image/jpeg')
allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('non-existant', 'master', true)
+ .to receive(:file).with('non-existant', 'master')
.and_return(nil)
end
@@ -178,9 +173,9 @@ describe ProjectWiki do
expect(subject.find_file('non-existant')).to eq(nil)
end
- it 'returns a Gollum::File instance' do
+ it 'returns a Gitlab::Git::WikiFile instance' do
file = subject.find_file('image.jpg')
- expect(file).to be_a Gollum::File
+ expect(file).to be_a Gitlab::Git::WikiFile
end
end
@@ -222,9 +217,9 @@ describe ProjectWiki do
describe "#update_page" do
before do
create_page("update-page", "some content")
- @gollum_page = subject.wiki.paged("update-page")
+ @gitlab_git_wiki_page = subject.wiki.page(title: "update-page")
subject.update_page(
- @gollum_page,
+ @gitlab_git_wiki_page,
content: "some other content",
format: :markdown,
message: "updated page"
@@ -246,7 +241,7 @@ describe ProjectWiki do
it 'updates project activity' do
subject.update_page(
- @gollum_page,
+ @gitlab_git_wiki_page,
content: 'Yet more content',
format: :markdown,
message: 'Updated page again'
@@ -262,7 +257,7 @@ describe ProjectWiki do
describe "#delete_page" do
before do
create_page("index", "some content")
- @page = subject.wiki.paged("index")
+ @page = subject.wiki.page(title: "index")
end
it "deletes the page" do
@@ -282,27 +277,28 @@ describe ProjectWiki do
describe '#create_repo!' do
it 'creates a repository' do
- expect(subject).to receive(:init_repo)
- .with(subject.full_path)
- .and_return(true)
-
+ expect(raw_repository.exists?).to eq(false)
expect(subject.repository).to receive(:after_create)
- expect(subject.create_repo!).to be_an_instance_of(Gollum::Wiki)
+ subject.send(:create_repo!, raw_repository)
+
+ expect(raw_repository.exists?).to eq(true)
end
end
describe '#ensure_repository' do
it 'creates the repository if it not exist' do
- allow(subject).to receive(:repository_exists?).and_return(false)
-
- expect(subject).to receive(:create_repo!)
+ expect(raw_repository.exists?).to eq(false)
+ expect(subject).to receive(:create_repo!).and_call_original
subject.ensure_repository
+
+ expect(raw_repository.exists?).to eq(true)
end
it 'does not create the repository if it exists' do
- allow(subject).to receive(:repository_exists?).and_return(true)
+ subject.wiki
+ expect(raw_repository.exists?).to eq(true)
expect(subject).not_to receive(:create_repo!)
@@ -329,7 +325,7 @@ describe ProjectWiki do
end
def commit_details
- { name: user.name, email: user.email, message: "test commit" }
+ Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
end
def create_page(name, content)
@@ -337,6 +333,6 @@ describe ProjectWiki do
end
def destroy_page(page)
- subject.wiki.delete_page(page, commit_details)
+ subject.delete_page(page, commit_details)
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 8a4dcdc311e..bdcdad46390 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -901,47 +901,6 @@ describe Repository do
end
end
- describe '#rm_branch' do
- let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
- let(:blank_sha) { '0000000000000000000000000000000000000000' }
-
- context 'when pre hooks were successful' do
- it 'runs without errors' do
- expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
-
- expect { repository.rm_branch(user, 'feature') }.not_to raise_error
- end
-
- it 'deletes the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
-
- expect { repository.rm_branch(user, 'feature') }.not_to raise_error
-
- expect(repository.find_branch('feature')).to be_nil
- end
- end
-
- context 'when pre hooks failed' do
- it 'gets an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- end
-
- it 'does not delete the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect do
- repository.rm_branch(user, 'feature')
- end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- expect(repository.find_branch('feature')).not_to be_nil
- end
- end
- end
-
describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
@@ -1744,13 +1703,75 @@ describe Repository do
end
describe '#rm_branch' do
- let(:user) { create(:user) }
+ shared_examples "user deleting a branch" do
+ it 'removes a branch' do
+ expect(repository).to receive(:before_remove_branch)
+ expect(repository).to receive(:after_remove_branch)
+
+ repository.rm_branch(user, 'feature')
+ end
+ end
- it 'removes a branch' do
- expect(repository).to receive(:before_remove_branch)
- expect(repository).to receive(:after_remove_branch)
+ context 'with gitaly enabled' do
+ it_behaves_like "user deleting a branch"
- repository.rm_branch(user, 'feature')
+ context 'when pre hooks failed' do
+ before do
+ allow_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError)
+ end
+
+ it 'gets an error and does not delete the branch' do
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
+ end
+
+ context 'with gitaly disabled', skip_gitaly_mock: true do
+ it_behaves_like "user deleting a branch"
+
+ let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+ let(:blank_sha) { '0000000000000000000000000000000000000000' }
+
+ context 'when pre hooks were successful' do
+ it 'runs without errors' do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
+ .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
+ end
+
+ it 'deletes the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
+
+ expect(repository.find_branch('feature')).to be_nil
+ end
+ end
+
+ context 'when pre hooks failed' do
+ it 'gets an error' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end
+
+ it 'does not delete the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 9ef8d117123..1f14d06997e 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -80,7 +80,7 @@ describe WikiPage do
context "when initialized with an existing gollum page" do
before do
create_page("test page", "test content")
- @page = wiki.wiki.paged("test page")
+ @page = wiki.wiki.page(title: "test page")
@wiki_page = described_class.new(wiki, @page, true)
end
@@ -105,7 +105,7 @@ describe WikiPage do
end
it "sets the version attribute" do
- expect(@wiki_page.version).to be_a Gollum::Git::Commit
+ expect(@wiki_page.version).to be_a Gitlab::Git::WikiPageVersion
end
end
end
@@ -321,14 +321,14 @@ describe WikiPage do
end
it 'returns true when requesting an old version' do
- old_version = @page.versions.last.to_s
+ old_version = @page.versions.last.id
old_page = wiki.find_page('Update', old_version)
expect(old_page.historical?).to eq true
end
it 'returns false when requesting latest version' do
- latest_version = @page.versions.first.to_s
+ latest_version = @page.versions.first.id
latest_page = wiki.find_page('Update', latest_version)
expect(latest_page.historical?).to eq false
@@ -393,7 +393,7 @@ describe WikiPage do
end
def commit_details
- { name: user.name, email: user.email, message: "test commit" }
+ Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
end
def create_page(name, content)
@@ -401,8 +401,8 @@ describe WikiPage do
end
def destroy_page(title)
- page = wiki.wiki.paged(title)
- wiki.wiki.delete_page(page, commit_details)
+ page = wiki.wiki.page(title: title)
+ wiki.delete_page(page, commit_details)
end
def get_slugs(page_or_dir)
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 2187be0190d..5e114434a67 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -300,6 +300,10 @@ describe MergeRequestPresenter do
described_class.new(resource, current_user: user).remove_wip_path
end
+ before do
+ allow(resource).to receive(:work_in_progress?).and_return(true)
+ end
+
context 'when merge request enabled and has permission' do
it 'has remove_wip_path' do
allow(project).to receive(:merge_requests_enabled?) { true }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 12720355a6d..5068df5b43a 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -360,6 +360,8 @@ describe API::Runner do
'policy' => 'pull-push' }]
end
+ let(:expected_features) { { 'trace_sections' => true } }
+
it 'picks a job' do
request_job info: { platform: :darwin }
@@ -379,6 +381,7 @@ describe API::Runner do
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
expect(json_response['variables']).to include(*expected_variables)
+ expect(json_response['features']).to eq(expected_features)
end
context 'when job is made for tag' do
diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb
new file mode 100644
index 00000000000..c589cd18f77
--- /dev/null
+++ b/spec/serializers/container_repository_entity_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe ContainerRepositoryEntity do
+ let(:entity) do
+ described_class.new(repository, request: request)
+ end
+
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:repository) { create(:container_repository, project: project) }
+
+ let(:request) { double('request') }
+
+ subject { entity.as_json }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ allow(request).to receive(:project).and_return(project)
+ allow(request).to receive(:current_user).and_return(user)
+ end
+
+ it 'exposes required informations' do
+ expect(subject).to include(:id, :path, :location, :tags_path)
+ end
+
+ context 'when user can manage repositories' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'exposes destroy_path' do
+ expect(subject).to include(:destroy_path)
+ end
+ end
+
+ context 'when user cannot manage repositories' do
+ it 'does not expose destroy_path' do
+ expect(subject).not_to include(:destroy_path)
+ end
+ end
+end
diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb
new file mode 100644
index 00000000000..6dcc5204516
--- /dev/null
+++ b/spec/serializers/container_tag_entity_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe ContainerTagEntity do
+ let(:entity) do
+ described_class.new(tag, request: request)
+ end
+
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:repository) { create(:container_repository, name: 'image', project: project) }
+
+ let(:request) { double('request') }
+ let(:tag) { repository.tag('test') }
+
+ subject { entity.as_json }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: /image/, tags: %w[test])
+ allow(request).to receive(:project).and_return(project)
+ allow(request).to receive(:current_user).and_return(user)
+ end
+
+ it 'exposes required informations' do
+ expect(subject).to include(:name, :location, :revision, :total_size, :created_at)
+ end
+
+ context 'when user can manage repositories' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'exposes destroy_path' do
+ expect(subject).to include(:destroy_path)
+ end
+ end
+
+ context 'when user cannot manage repositories' do
+ it 'does not expose destroy_path' do
+ expect(subject).not_to include(:destroy_path)
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 4288955ddbc..4aeb593da44 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -11,16 +11,6 @@ describe MergeRequestEntity do
described_class.new(resource, request: request).as_json
end
- it 'includes author' do
- req = double('request')
-
- author_payload = UserEntity
- .represent(resource.author, request: req)
- .as_json
-
- expect(subject[:author]).to eq(author_payload)
- end
-
it 'includes pipeline' do
req = double('request', current_user: user)
pipeline = build_stubbed(:ci_pipeline)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 2cc4643777e..dc89fdebce7 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -76,9 +76,8 @@ describe Projects::CreateService, '#execute' do
context 'wiki_enabled true creates wiki repository directory' do
it do
project = create_project(user, opts)
- path = ProjectWiki.new(project, user).send(:path_to_repo)
- expect(File.exist?(path)).to be_truthy
+ expect(wiki_repo(project).exists?).to be_truthy
end
end
@@ -86,11 +85,15 @@ describe Projects::CreateService, '#execute' do
it do
opts[:wiki_enabled] = false
project = create_project(user, opts)
- path = ProjectWiki.new(project, user).send(:path_to_repo)
- expect(File.exist?(path)).to be_falsey
+ expect(wiki_repo(project).exists?).to be_falsey
end
end
+
+ def wiki_repo(project)
+ relative_path = ProjectWiki.new(project).disk_path + '.git'
+ Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar')
+ end
end
context 'builds_enabled global setting' do
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 78a2ff73746..5f22d886910 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -39,11 +39,11 @@ module StubGitlabCalls
.and_return({ 'tags' => tags })
allow_any_instance_of(ContainerRegistry::Client)
- .to receive(:repository_manifest).with(repository)
+ .to receive(:repository_manifest).with(repository, anything)
.and_return(stub_container_registry_tag_manifest)
allow_any_instance_of(ContainerRegistry::Client)
- .to receive(:blob).with(repository)
+ .to receive(:blob).with(repository, anything, 'application/octet-stream')
.and_return(stub_container_registry_blob)
end
diff --git a/spec/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb
deleted file mode 100644
index cf0aa44a4a2..00000000000
--- a/spec/views/projects/registry/repositories/index.html.haml_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/registry/repositories/index' do
- let(:group) { create(:group, path: 'group') }
- let(:project) { create(:project, group: group, path: 'test') }
-
- let(:repository) do
- create(:container_repository, project: project, name: 'image')
- end
-
- before do
- stub_container_registry_config(enabled: true,
- host_port: 'registry.gitlab',
- api_url: 'http://registry.gitlab')
-
- stub_container_registry_tags(repository: :any, tags: [:latest])
-
- assign(:project, project)
- assign(:images, [repository])
-
- allow(view).to receive(:can?).and_return(true)
- end
-
- it 'contains container repository path' do
- render
-
- expect(rendered).to have_content 'group/test/image'
- end
-
- it 'contains attribute for copying tag location into clipboard' do
- render
-
- expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \
- 'registry.gitlab/group/test/image:latest"]'
- end
-end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index d2609d21546..1d9bbf2ca62 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -69,7 +69,12 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
def break_wiki(project)
- FileUtils.rm_rf(wiki_path(project) + '/objects')
+ objects_dir = wiki_path(project) + '/objects'
+
+ # Replace the /objects directory with a file so that the repo is
+ # invalid, _and_ 'git init' cannot fix it.
+ FileUtils.rm_rf(objects_dir)
+ FileUtils.touch(objects_dir) if File.directory?(wiki_path(project))
end
def wiki_path(project)