From e43b2e81dab3cade773d479f2ae56478e3113207 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Thu, 27 Oct 2016 17:39:32 -0200 Subject: Added MR Road map --- lib/container_registry/ROADMAP.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 lib/container_registry/ROADMAP.md diff --git a/lib/container_registry/ROADMAP.md b/lib/container_registry/ROADMAP.md new file mode 100644 index 00000000000..e0a20776404 --- /dev/null +++ b/lib/container_registry/ROADMAP.md @@ -0,0 +1,7 @@ +## Road map + +### Initial thoughts + +- Determine if image names will be persisted or fetched from API +- If persisted, how to update the stored names upon modification +- If fetched, how to fetch only images of a given project -- cgit v1.2.1 From dcd4beb8eb7bb7d0c2f720ef85c3da9f97a3dfe6 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 2 Nov 2016 00:33:35 -0200 Subject: Multi-level container image names backend implementation - Adds Registry events API endpoint - Adds container_images_repository and container_images models - Changes JWT authentication to allow multi-level scopes - Adds services for container image maintenance --- app/models/container_image.rb | 58 ++++++++++++++++++++++ app/models/container_images_repository.rb | 26 ++++++++++ app/models/project.rb | 1 + .../container_registry_authentication_service.rb | 9 +++- .../container_images/create_service.rb | 16 ++++++ .../container_images/destroy_service.rb | 11 ++++ .../container_images/push_service.rb | 26 ++++++++++ .../create_service.rb | 7 +++ ...029153736_create_container_images_repository.rb | 31 ++++++++++++ .../20161031013926_create_container_image.rb | 32 ++++++++++++ lib/api/api.rb | 1 + lib/api/registry_events.rb | 52 +++++++++++++++++++ lib/container_registry/client.rb | 4 ++ lib/container_registry/tag.rb | 8 ++- 14 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 app/models/container_image.rb create mode 100644 app/models/container_images_repository.rb create mode 100644 app/services/container_images_repositories/container_images/create_service.rb create mode 100644 app/services/container_images_repositories/container_images/destroy_service.rb create mode 100644 app/services/container_images_repositories/container_images/push_service.rb create mode 100644 app/services/container_images_repositories/create_service.rb create mode 100644 db/migrate/20161029153736_create_container_images_repository.rb create mode 100644 db/migrate/20161031013926_create_container_image.rb create mode 100644 lib/api/registry_events.rb diff --git a/app/models/container_image.rb b/app/models/container_image.rb new file mode 100644 index 00000000000..dcc4a7af629 --- /dev/null +++ b/app/models/container_image.rb @@ -0,0 +1,58 @@ +class ContainerImage < ActiveRecord::Base + belongs_to :container_images_repository + + delegate :registry, :registry_path_with_namespace, :client, to: :container_images_repository + + validates :manifest, presence: true + + before_validation :update_token, on: :create + def update_token + paths = container_images_repository.allowed_paths << name_with_namespace + token = Auth::ContainerRegistryAuthenticationService.full_access_token(paths) + client.update_token(token) + end + + def path + [registry.path, name_with_namespace].compact.join('/') + end + + def name_with_namespace + [registry_path_with_namespace, name].compact.join('/') + end + + def tag(tag) + ContainerRegistry::Tag.new(self, tag) + end + + def manifest + @manifest ||= client.repository_tags(name_with_namespace) + end + + def tags + return @tags if defined?(@tags) + return [] unless manifest && manifest['tags'] + + @tags = manifest['tags'].map do |tag| + ContainerRegistry::Tag.new(self, tag) + end + end + + def blob(config) + ContainerRegistry::Blob.new(self, config) + end + + def delete_tags + return unless tags + + tags.all?(&:delete) + end + + def self.split_namespace(full_path) + image_name = full_path.split('/').last + namespace = full_path.gsub(/(.*)(#{Regexp.escape('/' + image_name)})/, '\1') + if namespace.count('/') < 1 + namespace, image_name = full_path, "" + end + return namespace, image_name + end +end diff --git a/app/models/container_images_repository.rb b/app/models/container_images_repository.rb new file mode 100644 index 00000000000..99e94d2a6d0 --- /dev/null +++ b/app/models/container_images_repository.rb @@ -0,0 +1,26 @@ +class ContainerImagesRepository < ActiveRecord::Base + + belongs_to :project + + has_many :container_images, dependent: :destroy + + delegate :client, to: :registry + + def registry_path_with_namespace + project.path_with_namespace.downcase + end + + def allowed_paths + @allowed_paths ||= [registry_path_with_namespace] + + container_images.map { |i| i.name_with_namespace } + end + + def registry + @registry ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(allowed_paths) + url = Gitlab.config.registry.api_url + host_port = Gitlab.config.registry.host_port + ContainerRegistry::Registry.new(url, token: token, path: host_port) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 411299eef63..703e24eb79a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -157,6 +157,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete + has_one :container_images_repository, dependent: :destroy has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 5cb7a86a5ee..6b83b38fa4d 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -16,7 +16,7 @@ module Auth { token: authorized_token(scope).encoded } end - def self.full_access_token(*names) + def self.full_access_token(names) registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer @@ -61,7 +61,12 @@ module Auth end def process_repository_access(type, name, actions) - requested_project = Project.find_by_full_path(name) + # Strips image name due to lack of + # per image authentication. + # Removes only last occurence in light + # of future nested groups + namespace, _ = ContainerImage::split_namespace(name) + requested_project = Project.find_by_full_path(namespace) return unless requested_project actions = actions.select do |action| diff --git a/app/services/container_images_repositories/container_images/create_service.rb b/app/services/container_images_repositories/container_images/create_service.rb new file mode 100644 index 00000000000..0c2c69d5183 --- /dev/null +++ b/app/services/container_images_repositories/container_images/create_service.rb @@ -0,0 +1,16 @@ +module ContainerImagesRepositories + module ContainerImages + class CreateService < BaseService + def execute + @container_image = container_images_repository.container_images.create(params) + @container_image if @container_image.valid? + end + + private + + def container_images_repository + @container_images_repository ||= project.container_images_repository + end + end + end +end diff --git a/app/services/container_images_repositories/container_images/destroy_service.rb b/app/services/container_images_repositories/container_images/destroy_service.rb new file mode 100644 index 00000000000..91b8cfeea47 --- /dev/null +++ b/app/services/container_images_repositories/container_images/destroy_service.rb @@ -0,0 +1,11 @@ +module ContainerImagesRepositories + module ContainerImages + class DestroyService < BaseService + def execute(container_image) + return false unless container_image + + container_image.destroy + end + end + end +end diff --git a/app/services/container_images_repositories/container_images/push_service.rb b/app/services/container_images_repositories/container_images/push_service.rb new file mode 100644 index 00000000000..2731cf1d52e --- /dev/null +++ b/app/services/container_images_repositories/container_images/push_service.rb @@ -0,0 +1,26 @@ +module ContainerImagesRepositories + module ContainerImages + class PushService < BaseService + def execute(container_image_name, event) + find_or_create_container_image(container_image_name).valid? + end + + private + + def find_or_create_container_image(container_image_name) + options = {name: container_image_name} + container_images.find_by(options) || + ::ContainerImagesRepositories::ContainerImages::CreateService.new(project, + current_user, options).execute + end + + def container_images_repository + @container_images_repository ||= project.container_images_repository + end + + def container_images + @container_images ||= container_images_repository.container_images + end + end + end +end diff --git a/app/services/container_images_repositories/create_service.rb b/app/services/container_images_repositories/create_service.rb new file mode 100644 index 00000000000..7e9dd3abe5f --- /dev/null +++ b/app/services/container_images_repositories/create_service.rb @@ -0,0 +1,7 @@ +module ContainerImagesRepositories + class CreateService < BaseService + def execute + project.container_images_repository || ::ContainerImagesRepository.create(project: project) + end + end +end diff --git a/db/migrate/20161029153736_create_container_images_repository.rb b/db/migrate/20161029153736_create_container_images_repository.rb new file mode 100644 index 00000000000..d93180b1674 --- /dev/null +++ b/db/migrate/20161029153736_create_container_images_repository.rb @@ -0,0 +1,31 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateContainerImagesRepository < 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" 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" make sure that this + # method 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 fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :container_images_repositories do |t| + t.integer :project_id, null: false + end + end +end diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb new file mode 100644 index 00000000000..94feae280a6 --- /dev/null +++ b/db/migrate/20161031013926_create_container_image.rb @@ -0,0 +1,32 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateContainerImage < 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" 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" make sure that this + # method 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 fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :container_images do |t| + t.integer :container_images_repository_id + t.string :name + end + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index a0282ff8deb..ed775f898d2 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -84,6 +84,7 @@ module API mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings + mount ::API::RegistryEvents mount ::API::Pipelines mount ::API::ProjectHooks mount ::API::Projects diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb new file mode 100644 index 00000000000..c0473051424 --- /dev/null +++ b/lib/api/registry_events.rb @@ -0,0 +1,52 @@ +module API + # RegistryEvents API + class RegistryEvents < Grape::API + # before { authenticate! } + + content_type :json, 'application/vnd.docker.distribution.events.v1+json' + + params do + requires :events, type: Array, desc: 'The ID of a project' do + requires :id, type: String, desc: 'The ID of the event' + requires :timestamp, type: String, desc: 'Timestamp of the event' + requires :action, type: String, desc: 'Action performed by event' + requires :target, type: Hash, desc: 'Target of the event' do + optional :mediaType, type: String, desc: 'Media type of the target' + optional :size, type: Integer, desc: 'Size in bytes of the target' + requires :digest, type: String, desc: 'Digest of the target' + requires :repository, type: String, desc: 'Repository of target' + optional :url, type: String, desc: 'Url of the target' + optional :tag, type: String, desc: 'Tag of the target' + end + requires :request, type: Hash, desc: 'Request of the event' do + requires :id, type: String, desc: 'The ID of the request' + optional :addr, type: String, desc: 'IP Address of the request client' + optional :host, type: String, desc: 'Hostname of the registry instance' + requires :method, type: String, desc: 'Request method' + requires :useragent, type: String, desc: 'UserAgent header of the request' + end + requires :actor, type: Hash, desc: 'Actor that initiated the event' do + optional :name, type: String, desc: 'Actor name' + end + requires :source, type: Hash, desc: 'Source of the event' do + optional :addr, type: String, desc: 'Hostname of source registry node' + optional :instanceID, type: String, desc: 'Source registry node instanceID' + end + end + end + resource :registry_events do + post do + params['events'].each do |event| + repository = event['target']['repository'] + + if event['action'] == 'push' and !!event['target']['tag'] + namespace, container_image_name = ContainerImage::split_namespace(repository) + ::ContainerImagesRepositories::ContainerImages::PushService.new( + Project::find_with_namespace(namespace), current_user + ).execute(container_image_name, event) + end + end + end + end + end +end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 2edddb84fc3..2cbb7bfb67d 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -15,6 +15,10 @@ module ContainerRegistry @options = options end + def update_token(token) + @options[:token] = token + end + def repository_tags(name) response_body faraday.get("/v2/#{name}/tags/list") end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 59040199920..68dd87c979d 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -22,9 +22,7 @@ module ContainerRegistry end def manifest - return @manifest if defined?(@manifest) - - @manifest = client.repository_manifest(repository.name, name) + @manifest ||= client.repository_manifest(repository.name_with_namespace, name) end def path @@ -40,7 +38,7 @@ module ContainerRegistry def digest return @digest if defined?(@digest) - @digest = client.repository_tag_digest(repository.name, name) + @digest = client.repository_tag_digest(repository.name_with_namespace, name) end def config_blob @@ -82,7 +80,7 @@ module ContainerRegistry def delete return unless digest - client.delete_repository_tag(repository.name, digest) + client.delete_repository_tag(repository.name_with_namespace, digest) end end end -- cgit v1.2.1 From eed0b85ad084ad4d13cc26907102063d9372fe75 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 23 Nov 2016 14:50:30 -0200 Subject: First iteration of container_image view - Fixes project, container_image and tag deletion - Removed container_images_repository [ci skip] --- .../stylesheets/pages/container_registry.scss | 16 ++++++++++ .../projects/container_registry_controller.rb | 28 ++++++++++++----- app/models/container_image.rb | 20 +++++++++---- app/models/container_images_repository.rb | 26 ---------------- app/models/project.rb | 20 ++++++++----- app/services/container_images/destroy_service.rb | 32 ++++++++++++++++++++ .../container_images/create_service.rb | 16 ---------- .../container_images/destroy_service.rb | 11 ------- .../container_images/push_service.rb | 26 ---------------- .../create_service.rb | 7 ----- app/services/projects/destroy_service.rb | 10 ------- .../projects/container_registry/_image.html.haml | 35 ++++++++++++++++++++++ .../projects/container_registry/_tag.html.haml | 2 +- .../projects/container_registry/index.html.haml | 20 +++---------- ...029153736_create_container_images_repository.rb | 31 ------------------- .../20161031013926_create_container_image.rb | 2 +- lib/api/registry_events.rb | 16 ++++++++-- 17 files changed, 149 insertions(+), 169 deletions(-) create mode 100644 app/assets/stylesheets/pages/container_registry.scss delete mode 100644 app/models/container_images_repository.rb create mode 100644 app/services/container_images/destroy_service.rb delete mode 100644 app/services/container_images_repositories/container_images/create_service.rb delete mode 100644 app/services/container_images_repositories/container_images/destroy_service.rb delete mode 100644 app/services/container_images_repositories/container_images/push_service.rb delete mode 100644 app/services/container_images_repositories/create_service.rb create mode 100644 app/views/projects/container_registry/_image.html.haml delete mode 100644 db/migrate/20161029153736_create_container_images_repository.rb diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss new file mode 100644 index 00000000000..7d68eae3c97 --- /dev/null +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -0,0 +1,16 @@ +/** + * Container Registry + */ + +.container-image { + border-bottom: 1px solid #f0f0f0; +} + +.container-image-head { + padding: 0px 16px; + line-height: 4; +} + +.table.tags { + margin-bottom: 0px; +} diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index d1f46497207..54bcb5f504a 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -5,17 +5,22 @@ class Projects::ContainerRegistryController < Projects::ApplicationController layout 'project' def index - @tags = container_registry_repository.tags + @images = project.container_images end def destroy url = namespace_project_container_registry_index_path(project.namespace, project) - if tag.delete - redirect_to url + if tag + delete_tag(url) else - redirect_to url, alert: 'Failed to remove tag' + if image.destroy + redirect_to url + else + redirect_to url, alert: 'Failed to remove image' + end end + end private @@ -24,11 +29,20 @@ class Projects::ContainerRegistryController < Projects::ApplicationController render_404 unless Gitlab.config.registry.enabled end - def container_registry_repository - @container_registry_repository ||= project.container_registry_repository + def delete_tag(url) + if tag.delete + image.destroy if image.tags.empty? + redirect_to url + else + redirect_to url, alert: 'Failed to remove tag' + end + end + + def image + @image ||= project.container_images.find_by(id: params[:id]) end def tag - @tag ||= container_registry_repository.tag(params[:id]) + @tag ||= image.tag(params[:tag]) if params[:tag].present? end end diff --git a/app/models/container_image.rb b/app/models/container_image.rb index dcc4a7af629..7721c53a6fc 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -1,23 +1,28 @@ class ContainerImage < ActiveRecord::Base - belongs_to :container_images_repository + belongs_to :project - delegate :registry, :registry_path_with_namespace, :client, to: :container_images_repository + delegate :container_registry, :container_registry_allowed_paths, + :container_registry_path_with_namespace, to: :project + + delegate :client, to: :container_registry validates :manifest, presence: true + before_destroy :delete_tags + before_validation :update_token, on: :create def update_token - paths = container_images_repository.allowed_paths << name_with_namespace + paths = container_registry_allowed_paths << name_with_namespace token = Auth::ContainerRegistryAuthenticationService.full_access_token(paths) client.update_token(token) end def path - [registry.path, name_with_namespace].compact.join('/') + [container_registry.path, name_with_namespace].compact.join('/') end def name_with_namespace - [registry_path_with_namespace, name].compact.join('/') + [container_registry_path_with_namespace, name].compact.join('/') end def tag(tag) @@ -44,7 +49,10 @@ class ContainerImage < ActiveRecord::Base def delete_tags return unless tags - tags.all?(&:delete) + digests = tags.map {|tag| tag.digest }.to_set + digests.all? do |digest| + client.delete_repository_tag(name_with_namespace, digest) + end end def self.split_namespace(full_path) diff --git a/app/models/container_images_repository.rb b/app/models/container_images_repository.rb deleted file mode 100644 index 99e94d2a6d0..00000000000 --- a/app/models/container_images_repository.rb +++ /dev/null @@ -1,26 +0,0 @@ -class ContainerImagesRepository < ActiveRecord::Base - - belongs_to :project - - has_many :container_images, dependent: :destroy - - delegate :client, to: :registry - - def registry_path_with_namespace - project.path_with_namespace.downcase - end - - def allowed_paths - @allowed_paths ||= [registry_path_with_namespace] + - container_images.map { |i| i.name_with_namespace } - end - - def registry - @registry ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(allowed_paths) - url = Gitlab.config.registry.api_url - host_port = Gitlab.config.registry.host_port - ContainerRegistry::Registry.new(url, token: token, path: host_port) - end - end -end diff --git a/app/models/project.rb b/app/models/project.rb index 703e24eb79a..afaf2095a4c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -157,7 +157,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete - has_one :container_images_repository, dependent: :destroy + has_many :container_images, dependent: :destroy has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -405,15 +405,19 @@ class Project < ActiveRecord::Base path_with_namespace.downcase end - def container_registry_repository + def container_registry_allowed_paths + @container_registry_allowed_paths ||= [container_registry_path_with_namespace] + + container_images.map { |i| i.name_with_namespace } + end + + def container_registry return unless Gitlab.config.registry.enabled - @container_registry_repository ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace) + @container_registry ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_allowed_paths) url = Gitlab.config.registry.api_url host_port = Gitlab.config.registry.host_port - registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) - registry.repository(container_registry_path_with_namespace) + ContainerRegistry::Registry.new(url, token: token, path: host_port) end end @@ -424,9 +428,9 @@ class Project < ActiveRecord::Base end def has_container_registry_tags? - return unless container_registry_repository + return unless container_images - container_registry_repository.tags.any? + container_images.first.tags.any? end def commit(ref = 'HEAD') diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb new file mode 100644 index 00000000000..bc5b53fd055 --- /dev/null +++ b/app/services/container_images/destroy_service.rb @@ -0,0 +1,32 @@ +module ContainerImages + class DestroyService < BaseService + + class DestroyError < StandardError; end + + def execute(container_image) + @container_image = container_image + + return false unless can?(current_user, :remove_project, project) + + ContainerImage.transaction do + container_image.destroy! + + unless remove_container_image_tags + raise_error('Failed to remove container image tags. Please try again or contact administrator') + end + end + + true + end + + private + + def raise_error(message) + raise DestroyError.new(message) + end + + def remove_container_image_tags + container_image.delete_tags + end + end +end diff --git a/app/services/container_images_repositories/container_images/create_service.rb b/app/services/container_images_repositories/container_images/create_service.rb deleted file mode 100644 index 0c2c69d5183..00000000000 --- a/app/services/container_images_repositories/container_images/create_service.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ContainerImagesRepositories - module ContainerImages - class CreateService < BaseService - def execute - @container_image = container_images_repository.container_images.create(params) - @container_image if @container_image.valid? - end - - private - - def container_images_repository - @container_images_repository ||= project.container_images_repository - end - end - end -end diff --git a/app/services/container_images_repositories/container_images/destroy_service.rb b/app/services/container_images_repositories/container_images/destroy_service.rb deleted file mode 100644 index 91b8cfeea47..00000000000 --- a/app/services/container_images_repositories/container_images/destroy_service.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ContainerImagesRepositories - module ContainerImages - class DestroyService < BaseService - def execute(container_image) - return false unless container_image - - container_image.destroy - end - end - end -end diff --git a/app/services/container_images_repositories/container_images/push_service.rb b/app/services/container_images_repositories/container_images/push_service.rb deleted file mode 100644 index 2731cf1d52e..00000000000 --- a/app/services/container_images_repositories/container_images/push_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module ContainerImagesRepositories - module ContainerImages - class PushService < BaseService - def execute(container_image_name, event) - find_or_create_container_image(container_image_name).valid? - end - - private - - def find_or_create_container_image(container_image_name) - options = {name: container_image_name} - container_images.find_by(options) || - ::ContainerImagesRepositories::ContainerImages::CreateService.new(project, - current_user, options).execute - end - - def container_images_repository - @container_images_repository ||= project.container_images_repository - end - - def container_images - @container_images ||= container_images_repository.container_images - end - end - end -end diff --git a/app/services/container_images_repositories/create_service.rb b/app/services/container_images_repositories/create_service.rb deleted file mode 100644 index 7e9dd3abe5f..00000000000 --- a/app/services/container_images_repositories/create_service.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ContainerImagesRepositories - class CreateService < BaseService - def execute - project.container_images_repository || ::ContainerImagesRepository.create(project: project) - end - end -end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 9716a1780a9..ba410b79e8c 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -31,10 +31,6 @@ module Projects project.team.truncate project.destroy! - unless remove_registry_tags - raise_error('Failed to remove project container registry. Please try again or contact administrator') - end - unless remove_repository(repo_path) raise_error('Failed to remove project repository. Please try again or contact administrator') end @@ -68,12 +64,6 @@ module Projects end end - def remove_registry_tags - return true unless Gitlab.config.registry.enabled - - project.container_registry_repository.delete_tags - end - def raise_error(message) raise DestroyError.new(message) end diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml new file mode 100644 index 00000000000..b1d62e34a97 --- /dev/null +++ b/app/views/projects/container_registry/_image.html.haml @@ -0,0 +1,35 @@ +- expanded = false +.container-image.js-toggle-container + .container-image-head + = link_to "#", class: "js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + = escape_once(image.name) + = clipboard_button(clipboard_text: "docker pull #{image.path}") + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = icon("trash cred") + + + .container-image-tags.js-toggle-content{ class: ("hide" unless expanded) } + - if image.tags.blank? + %li + .nothing-here-block No tags in Container Registry for this container image. + + - else + .table-holder + %table.table.tags + %thead + %tr + %th Name + %th Image ID + %th Size + %th Created + - if can?(current_user, :update_container_image, @project) + %th + + - image.tags.each do |tag| + = render 'tag', tag: tag diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index 10822b6184c..00345ec26de 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -25,5 +25,5 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 993da27310f..f074ce6be6d 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -19,21 +19,9 @@ %br docker push #{escape_once(@project.container_registry_repository_url)} - - if @tags.blank? - %li - .nothing-here-block No images in Container Registry for this project. + - if @images.blank? + .nothing-here-block No container images in Container Registry for this project. - else - .table-holder - %table.table.tags - %thead - %tr - %th Name - %th Image ID - %th Size - %th Created - - if can?(current_user, :update_container_image, @project) - %th - - - @tags.each do |tag| - = render 'tag', tag: tag + - @images.each do |image| + = render 'image', image: image diff --git a/db/migrate/20161029153736_create_container_images_repository.rb b/db/migrate/20161029153736_create_container_images_repository.rb deleted file mode 100644 index d93180b1674..00000000000 --- a/db/migrate/20161029153736_create_container_images_repository.rb +++ /dev/null @@ -1,31 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class CreateContainerImagesRepository < 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" 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" make sure that this - # method 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 fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - - def change - create_table :container_images_repositories do |t| - t.integer :project_id, null: false - end - end -end diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb index 94feae280a6..85c0913b8f3 100644 --- a/db/migrate/20161031013926_create_container_image.rb +++ b/db/migrate/20161031013926_create_container_image.rb @@ -25,7 +25,7 @@ class CreateContainerImage < ActiveRecord::Migration def change create_table :container_images do |t| - t.integer :container_images_repository_id + t.integer :project_id t.string :name end end diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index c0473051424..dc7279d2b75 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -41,9 +41,19 @@ module API if event['action'] == 'push' and !!event['target']['tag'] namespace, container_image_name = ContainerImage::split_namespace(repository) - ::ContainerImagesRepositories::ContainerImages::PushService.new( - Project::find_with_namespace(namespace), current_user - ).execute(container_image_name, event) + project = Project::find_with_namespace(namespace) + + if project + container_image = project.container_images.find_or_create_by(name: container_image_name) + + if container_image.valid? + puts('Valid!') + else + render_api_error!({ error: "Failed to create container image!" }, 400) + end + else + not_found!('Project') + end end end end -- cgit v1.2.1 From 246df2bd1151d39a04ef553064144eb75ee3e980 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Tue, 13 Dec 2016 23:42:43 -0200 Subject: Adding registry endpoint authorization --- .../admin/application_settings_controller.rb | 6 +++++ .../admin/container_registry_controller.rb | 11 ++++++++ app/models/application_setting.rb | 6 +++++ app/views/admin/container_registry/show.html.haml | 31 ++++++++++++++++++++++ app/views/admin/dashboard/_head.html.haml | 4 +++ config/routes/admin.rb | 2 ++ ...egistry_access_token_to_application_settings.rb | 29 ++++++++++++++++++++ doc/administration/container_registry.md | 22 +++++++++++++-- doc/ci/docker/using_docker_build.md | 8 +++--- doc/user/project/container_registry.md | 19 +++++++------ lib/api/helpers.rb | 10 +++++++ lib/api/registry_events.rb | 2 +- 12 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 app/controllers/admin/container_registry_controller.rb create mode 100644 app/views/admin/container_registry/show.html.haml create mode 100644 db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index b0f5d4a9933..fb6df1a06d2 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -29,6 +29,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to :back end + def reset_container_registry_token + @application_setting.reset_container_registry_access_token! + flash[:notice] = 'New container registry access token has been generated!' + redirect_to :back + end + def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async diff --git a/app/controllers/admin/container_registry_controller.rb b/app/controllers/admin/container_registry_controller.rb new file mode 100644 index 00000000000..265c032c67d --- /dev/null +++ b/app/controllers/admin/container_registry_controller.rb @@ -0,0 +1,11 @@ +class Admin::ContainerRegistryController < Admin::ApplicationController + def show + @access_token = container_registry_access_token + end + + private + + def container_registry_access_token + current_application_settings.container_registry_access_token + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 74b358d8c40..b94a71e1ea7 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -4,6 +4,7 @@ class ApplicationSetting < ActiveRecord::Base add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token + add_authentication_token_field :container_registry_access_token CACHE_KEY = 'application_setting.last' DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace @@ -141,6 +142,7 @@ class ApplicationSetting < ActiveRecord::Base before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token + before_save :ensure_container_registry_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -276,6 +278,10 @@ class ApplicationSetting < ActiveRecord::Base ensure_health_check_access_token! end + def container_registry_access_token + ensure_container_registry_access_token! + end + def sidekiq_throttling_enabled? return false unless sidekiq_throttling_column_exists? diff --git a/app/views/admin/container_registry/show.html.haml b/app/views/admin/container_registry/show.html.haml new file mode 100644 index 00000000000..8803eddda69 --- /dev/null +++ b/app/views/admin/container_registry/show.html.haml @@ -0,0 +1,31 @@ +- @no_container = true += render "admin/dashboard/head" + +%div{ class: container_class } + + %p.prepend-top-default + %span + To properly configure the Container Registry you should add the following + access token to the Docker Registry config.yml as follows: + %pre + %code + :plain + notifications: + endpoints: + - ... + headers: + X-Registry-Token: [#{@access_token}] + %br + Access token is + %code{ id: 'registry-token' } #{@access_token} + + .bs-callout.clearfix + .pull-left + %p + You can reset container registry access token by pressing the button below. + %p + = button_to reset_container_registry_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset container registry token?' } do + = icon('refresh') + Reset container registry access token diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index 7893c1dee97..dbd039547fa 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -27,3 +27,7 @@ = link_to admin_runners_path, title: 'Runners' do %span Runners + = nav_link path: 'container_registry#show' do + = link_to admin_container_registry_path, title: 'Registry' do + %span + Registry diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 8e99239f350..b09c05826a7 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -58,6 +58,7 @@ namespace :admin do resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } + resource :container_registry, controller: 'container_registry', only: [:show] resources :projects, only: [:index] @@ -88,6 +89,7 @@ namespace :admin do resources :services, only: [:index, :edit, :update] put :reset_runners_token put :reset_health_check_token + put :reset_container_registry_token put :clear_repository_check_states end diff --git a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb new file mode 100644 index 00000000000..f89f9b00a5f --- /dev/null +++ b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddContainerRegistryAccessTokenToApplicationSettings < 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" 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" make sure that this + # method 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 fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :application_settings, :container_registry_access_token, :string + end +end diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index a6300e18dc0..14795601246 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -76,7 +76,7 @@ you modify its settings. Read the upstream documentation on how to achieve that. At the absolute minimum, make sure your [Registry configuration][registry-auth] has `container_registry` as the service and `https://gitlab.example.com/jwt/auth` -as the realm: +as the realm. ``` auth: @@ -87,6 +87,23 @@ auth: rootcertbundle: /root/certs/certbundle ``` +Also a notification endpoint must be configured with the token from +Admin Area -> Overview -> Registry (`/admin/container_registry`) like in the following sample: + +``` +notifications: + endpoints: + - name: listener + url: https://gitlab.example.com/api/v3/registry_events + headers: + X-Registry-Token: [57Cx95fc2zHFh93VTiGD] + timeout: 500ms + threshold: 5 + backoff: 1s +``` + +Check the [Registry endpoint configuration][registry-endpoint] for details. + ## Container Registry domain configuration There are two ways you can configure the Registry's external domain. @@ -477,7 +494,7 @@ configurable in future releases. **GitLab 8.8 ([source docs][8-8-docs])** - GitLab Container Registry feature was introduced. - +i [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure [restart gitlab]: restart_gitlab.md#installations-from-source [wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate @@ -487,6 +504,7 @@ configurable in future releases. [storage-config]: https://docs.docker.com/registry/configuration/#storage [registry-http-config]: https://docs.docker.com/registry/configuration/#http [registry-auth]: https://docs.docker.com/registry/configuration/#auth +[registry-endpoint]: https://docs.docker.com/registry/notifications/#/configuration [token-config]: https://docs.docker.com/registry/configuration/#token [8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 8620984d40d..6ae6269b28a 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -299,8 +299,8 @@ could look like: stage: build script: - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com - - docker build -t registry.example.com/group/project:latest . - - docker push registry.example.com/group/project:latest + - docker build -t registry.example.com/group/project/image:latest . + - docker push registry.example.com/group/project/image:latest ``` You have to use the special `gitlab-ci-token` user created for you in order to @@ -350,8 +350,8 @@ stages: - deploy variables: - CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_BUILD_REF_NAME - CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest + CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_BUILD_REF_NAME + CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest before_script: - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 91b35c73b34..eada8e04227 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -10,6 +10,7 @@ - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need to pass a personal access token instead of your password in order to login to GitLab's Container Registry. +- Multiple level image names support was added in GitLab ?8.15? With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. @@ -54,26 +55,23 @@ sure that you are using the Registry URL with the namespace and project name that is hosted on GitLab: ``` -docker build -t registry.example.com/group/project . -docker push registry.example.com/group/project +docker build -t registry.example.com/group/project/image . +docker push registry.example.com/group/project/image ``` Your image will be named after the following scheme: ``` -// +/// ``` -As such, the name of the image is unique, but you can differentiate the images -using tags. - ## Use images from GitLab Container Registry To download and run a container from images hosted in GitLab Container Registry, use `docker run`: ``` -docker run [options] registry.example.com/group/project [arguments] +docker run [options] registry.example.com/group/project/image [arguments] ``` For more information on running Docker containers, visit the @@ -87,7 +85,8 @@ and click **Registry** in the project menu. This view will show you all tags in your project and will easily allow you to delete them. -![Container Registry panel](img/container_registry_panel.png) +![Container Registry panel](image-needs-update) +[//]: # (img/container_registry_panel.png) ## Build and push images using GitLab CI @@ -136,7 +135,7 @@ A user attempted to enable an S3-backed Registry. The `docker login` step went fine. However, when pushing an image, the output showed: ``` -The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test] +The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test/docker-image] dc5e59c14160: Pushing [==================================================>] 14.85 kB 03c20c1a019a: Pushing [==================================================>] 2.048 kB a08f14ef632e: Pushing [==================================================>] 2.048 kB @@ -229,7 +228,7 @@ a container image. You may need to run as root to do this. For example: ```sh docker login s3-testing.myregistry.com:4567 -docker push s3-testing.myregistry.com:4567/root/docker-test +docker push s3-testing.myregistry.com:4567/root/docker-test/docker-image ``` In the example above, we see the following trace on the mitmproxy window: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a1db2099693..0fd2b1587e3 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -111,6 +111,16 @@ module API end end + def authenticate_container_registry_access_token! + token = request.headers['X-Registry-Token'] + unless token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare( + token, + current_application_settings.container_registry_access_token + ) + unauthorized! + end + end + def authenticated_as_admin! authenticate! forbidden! unless current_user.is_admin? diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index dc7279d2b75..e52433339eb 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -1,7 +1,7 @@ module API # RegistryEvents API class RegistryEvents < Grape::API - # before { authenticate! } + before { authenticate_container_registry_access_token! } content_type :json, 'application/vnd.docker.distribution.events.v1+json' -- cgit v1.2.1 From e4fa80f3b67f1ef30c262cd4df28516ccff6336a Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Fri, 16 Dec 2016 01:24:05 -0200 Subject: Fixes broken and missing tests --- .../stylesheets/pages/container_registry.scss | 6 +- .../projects/container_registry_controller.rb | 1 - app/models/container_image.rb | 4 +- app/models/namespace.rb | 8 +-- app/models/project.rb | 18 ++---- .../container_registry_authentication_service.rb | 3 +- app/services/container_images/destroy_service.rb | 1 - app/services/projects/transfer_service.rb | 6 +- .../projects/container_registry/_image.html.haml | 2 +- .../projects/container_registry/_tag.html.haml | 2 +- .../projects/container_registry/index.html.haml | 4 +- db/schema.rb | 6 ++ lib/api/registry_events.rb | 4 +- lib/container_registry/blob.rb | 4 +- lib/container_registry/registry.rb | 4 -- lib/container_registry/repository.rb | 48 -------------- spec/factories/container_images.rb | 21 +++++++ spec/features/container_registry_spec.rb | 32 +++++++--- .../security/project/internal_access_spec.rb | 3 + .../security/project/private_access_spec.rb | 3 + .../security/project/public_access_spec.rb | 3 + spec/lib/container_registry/blob_spec.rb | 15 +++-- spec/lib/container_registry/registry_spec.rb | 2 +- spec/lib/container_registry/repository_spec.rb | 65 ------------------- spec/lib/container_registry/tag_spec.rb | 11 +++- spec/lib/gitlab/import_export/all_models.yml | 3 + spec/models/ci/build_spec.rb | 2 +- spec/models/container_image_spec.rb | 73 ++++++++++++++++++++++ spec/models/namespace_spec.rb | 8 ++- spec/models/project_spec.rb | 41 +++--------- spec/services/projects/destroy_service_spec.rb | 15 +++-- spec/services/projects/transfer_service_spec.rb | 3 + 32 files changed, 211 insertions(+), 210 deletions(-) delete mode 100644 lib/container_registry/repository.rb create mode 100644 spec/factories/container_images.rb delete mode 100644 spec/lib/container_registry/repository_spec.rb create mode 100644 spec/models/container_image_spec.rb diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index 7d68eae3c97..92543d7d714 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -3,14 +3,14 @@ */ .container-image { - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid $white-normal; } .container-image-head { - padding: 0px 16px; + padding: 0 16px; line-height: 4; } .table.tags { - margin-bottom: 0px; + margin-bottom: 0; } diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index 54bcb5f504a..f656f86fcdb 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -20,7 +20,6 @@ class Projects::ContainerRegistryController < Projects::ApplicationController redirect_to url, alert: 'Failed to remove image' end end - end private diff --git a/app/models/container_image.rb b/app/models/container_image.rb index 7721c53a6fc..583cb977910 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -22,7 +22,7 @@ class ContainerImage < ActiveRecord::Base end def name_with_namespace - [container_registry_path_with_namespace, name].compact.join('/') + [container_registry_path_with_namespace, name].reject(&:blank?).join('/') end def tag(tag) @@ -55,6 +55,8 @@ class ContainerImage < ActiveRecord::Base end end + # rubocop:disable RedundantReturn + def self.split_namespace(full_path) image_name = full_path.split('/').last namespace = full_path.gsub(/(.*)(#{Regexp.escape('/' + image_name)})/, '\1') diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bd0336c984a..c8e329044e0 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -118,8 +118,8 @@ class Namespace < ActiveRecord::Base end def move_dir - if any_project_has_container_registry_tags? - raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') + if any_project_has_container_registry_images? + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has images in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -154,8 +154,8 @@ class Namespace < ActiveRecord::Base end end - def any_project_has_container_registry_tags? - projects.any?(&:has_container_registry_tags?) + def any_project_has_container_registry_images? + projects.any? { |project| project.container_images.present? } end def send_update_instructions diff --git a/app/models/project.rb b/app/models/project.rb index afaf2095a4c..d4f5584f53d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -421,18 +421,12 @@ class Project < ActiveRecord::Base end end - def container_registry_repository_url + def container_registry_url if Gitlab.config.registry.enabled "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}" end end - def has_container_registry_tags? - return unless container_images - - container_images.first.tags.any? - end - def commit(ref = 'HEAD') repository.commit(ref) end @@ -913,11 +907,11 @@ class Project < ActiveRecord::Base expire_caches_before_rename(old_path_with_namespace) - if has_container_registry_tags? - Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" + if container_images.present? + Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry images are present" - # we currently doesn't support renaming repository if it contains tags in container registry - raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') + # we currently doesn't support renaming repository if it contains images in container registry + raise StandardError.new('Project cannot be renamed, because images are present in its container registry') end if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) @@ -1264,7 +1258,7 @@ class Project < ActiveRecord::Base ] if container_registry_enabled? - variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true } + variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true } end variables diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 6b83b38fa4d..5b2fcdf3b16 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -16,7 +16,8 @@ module Auth { token: authorized_token(scope).encoded } end - def self.full_access_token(names) + def self.full_access_token(*names) + names = names.flatten registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb index bc5b53fd055..c73b6cfefba 100644 --- a/app/services/container_images/destroy_service.rb +++ b/app/services/container_images/destroy_service.rb @@ -1,6 +1,5 @@ module ContainerImages class DestroyService < BaseService - class DestroyError < StandardError; end def execute(container_image) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 20dfbddc823..3e241b9e7c0 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -36,9 +36,9 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - if project.has_container_registry_tags? - # we currently doesn't support renaming repository if it contains tags in container registry - raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') + unless project.container_images.empty? + # we currently doesn't support renaming repository if it contains images in container registry + raise TransferError.new('Project cannot be transferred, because images are present in its container registry') end project.expire_caches_before_rename(old_path) diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml index b1d62e34a97..5845efd345a 100644 --- a/app/views/projects/container_registry/_image.html.haml +++ b/app/views/projects/container_registry/_image.html.haml @@ -10,7 +10,7 @@ = escape_once(image.name) = clipboard_button(clipboard_text: "docker pull #{image.path}") .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove image", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index 00345ec26de..b35a9cb621f 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -25,5 +25,5 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index f074ce6be6d..ab6213f03d8 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -15,9 +15,9 @@ %br Then you are free to create and upload a container image with build and push commands: %pre - docker build -t #{escape_once(@project.container_registry_repository_url)} . + docker build -t #{escape_once(@project.container_registry_url)} . %br - docker push #{escape_once(@project.container_registry_repository_url)} + docker push #{escape_once(@project.container_registry_url)} - if @images.blank? .nothing-here-block No container images in Container Registry for this project. diff --git a/db/schema.rb b/db/schema.rb index 88aaa6c3c55..36df20fc8f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.string "container_registry_access_token" t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -392,6 +393,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree + create_table "container_images", force: :cascade do |t| + t.integer "project_id" + t.string "name" + end + create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index e52433339eb..12305a49f0f 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -46,9 +46,7 @@ module API if project container_image = project.container_images.find_or_create_by(name: container_image_name) - if container_image.valid? - puts('Valid!') - else + unless container_image.valid? render_api_error!({ error: "Failed to create container image!" }, 400) end else diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb index eb5a2596177..8db8e483b1d 100644 --- a/lib/container_registry/blob.rb +++ b/lib/container_registry/blob.rb @@ -38,11 +38,11 @@ module ContainerRegistry end def delete - client.delete_blob(repository.name, digest) + client.delete_blob(repository.name_with_namespace, digest) end def data - @data ||= client.blob(repository.name, digest, type) + @data ||= client.blob(repository.name_with_namespace, digest, type) end end end diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb index 0e634f6b6ef..63bce655f57 100644 --- a/lib/container_registry/registry.rb +++ b/lib/container_registry/registry.rb @@ -8,10 +8,6 @@ module ContainerRegistry @client = ContainerRegistry::Client.new(uri, options) end - def repository(name) - ContainerRegistry::Repository.new(self, name) - end - private def default_path diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb deleted file mode 100644 index 0e4a7cb3cc9..00000000000 --- a/lib/container_registry/repository.rb +++ /dev/null @@ -1,48 +0,0 @@ -module ContainerRegistry - class Repository - attr_reader :registry, :name - - delegate :client, to: :registry - - def initialize(registry, name) - @registry, @name = registry, name - end - - def path - [registry.path, name].compact.join('/') - end - - def tag(tag) - ContainerRegistry::Tag.new(self, tag) - end - - def manifest - return @manifest if defined?(@manifest) - - @manifest = client.repository_tags(name) - end - - def valid? - manifest.present? - end - - def tags - return @tags if defined?(@tags) - return [] unless manifest && manifest['tags'] - - @tags = manifest['tags'].map do |tag| - ContainerRegistry::Tag.new(self, tag) - end - end - - def blob(config) - ContainerRegistry::Blob.new(self, config) - end - - def delete_tags - return unless tags - - tags.all?(&:delete) - end - end -end diff --git a/spec/factories/container_images.rb b/spec/factories/container_images.rb new file mode 100644 index 00000000000..6141a519a75 --- /dev/null +++ b/spec/factories/container_images.rb @@ -0,0 +1,21 @@ +FactoryGirl.define do + factory :container_image do + name "test_container_image" + project + + transient do + tags ['tag'] + stubbed true + end + + after(:build) do |image, evaluator| + if evaluator.stubbed + allow(Gitlab.config.registry).to receive(:enabled).and_return(true) + allow(image.client).to receive(:repository_tags).and_return({ + name: image.name_with_namespace, + tags: evaluator.tags + }) + end + end + end +end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 203e55a36f2..862c9fbf6c0 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -2,15 +2,18 @@ require 'spec_helper' describe "Container Registry" do let(:project) { create(:empty_project) } - let(:repository) { project.container_registry_repository } + let(:registry) { project.container_registry } let(:tag_name) { 'latest' } let(:tags) { [tag_name] } + let(:container_image) { create(:container_image) } + let(:image_name) { container_image.name } before do login_as(:user) project.team << [@user, :developer] - stub_container_registry_tags(*tags) stub_container_registry_config(enabled: true) + stub_container_registry_tags(*tags) + project.container_images << container_image unless container_image.nil? allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') end @@ -19,15 +22,26 @@ describe "Container Registry" do visit namespace_project_container_registry_index_path(project.namespace, project) end - context 'when no tags' do - let(:tags) { [] } + context 'when no images' do + let(:container_image) { } + + it { expect(page).to have_content('No container images in Container Registry for this project') } + end - it { expect(page).to have_content('No images in Container Registry for this project') } + context 'when there are images' do + it { expect(page).to have_content(image_name) } end + end + + describe 'DELETE /:project/container_registry/:image_id' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + it do + expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(true) - context 'when there are tags' do - it { expect(page).to have_content(tag_name) } - it { expect(page).to have_content('d7a513a66') } + click_on 'Remove image' end end @@ -39,7 +53,7 @@ describe "Container Registry" do it do expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) - click_on 'Remove' + click_on 'Remove tag' end end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 24af062d763..4e7a2c0ecc0 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -429,9 +429,12 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/container_registry" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_tags('latest') stub_container_registry_config(enabled: true) + project.container_images << container_image end subject { namespace_project_container_registry_index_path(project.namespace, project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index c511dcfa18e..c74cdc05593 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -418,9 +418,12 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/container_registry" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_tags('latest') stub_container_registry_config(enabled: true) + project.container_images << container_image end subject { namespace_project_container_registry_index_path(project.namespace, project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index d8cc012c27e..485ef335b78 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -429,9 +429,12 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/container_registry" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_tags('latest') stub_container_registry_config(enabled: true) + project.container_images << container_image end subject { namespace_project_container_registry_index_path(project.namespace, project) } diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index bbacdc67ebd..f092449c4bd 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -9,12 +9,19 @@ describe ContainerRegistry::Blob do 'size' => 1000 } end - let(:token) { 'authorization-token' } - - let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) } - let(:repository) { registry.repository('group/test') } + let(:token) { 'token' } + + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + let(:repository) { create(:container_image, name: '', project: project) } let(:blob) { repository.blob(config) } + before do + stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host) + end + it { expect(blob).to respond_to(:repository) } it { expect(blob).to delegate_method(:registry).to(:repository) } it { expect(blob).to delegate_method(:client).to(:repository) } diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb index 4f3f8b24fc4..4d6eea94bf0 100644 --- a/spec/lib/container_registry/registry_spec.rb +++ b/spec/lib/container_registry/registry_spec.rb @@ -10,7 +10,7 @@ describe ContainerRegistry::Registry do it { is_expected.to respond_to(:uri) } it { is_expected.to respond_to(:path) } - it { expect(subject.repository('test')).not_to be_nil } + it { expect(subject).not_to be_nil } context '#path' do subject { registry.path } diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb deleted file mode 100644 index c364e759108..00000000000 --- a/spec/lib/container_registry/repository_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe ContainerRegistry::Repository do - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } - let(:repository) { registry.repository('group/test') } - - it { expect(repository).to respond_to(:registry) } - it { expect(repository).to delegate_method(:client).to(:registry) } - it { expect(repository.tag('test')).not_to be_nil } - - context '#path' do - subject { repository.path } - - it { is_expected.to eq('example.com/group/test') } - end - - context 'manifest processing' do - before do - stub_request(:get, 'http://example.com/v2/group/test/tags/list'). - with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). - to_return( - status: 200, - body: JSON.dump(tags: ['test']), - headers: { 'Content-Type' => 'application/json' }) - end - - context '#manifest' do - subject { repository.manifest } - - it { is_expected.not_to be_nil } - end - - context '#valid?' do - subject { repository.valid? } - - it { is_expected.to be_truthy } - end - - context '#tags' do - subject { repository.tags } - - it { is_expected.not_to be_empty } - end - end - - context '#delete_tags' do - let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') } - - before { expect(repository).to receive(:tags).twice.and_return([tag]) } - - subject { repository.delete_tags } - - context 'succeeds' do - before { expect(tag).to receive(:delete).and_return(true) } - - it { is_expected.to be_truthy } - end - - context 'any fails' do - before { expect(tag).to receive(:delete).and_return(false) } - - it { is_expected.to be_falsey } - end - end -end diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index c5e31ae82b6..cdd0fe66bc3 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -1,11 +1,18 @@ require 'spec_helper' describe ContainerRegistry::Tag do - let(:registry) { ContainerRegistry::Registry.new('http://example.com') } - let(:repository) { registry.repository('group/test') } + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + let(:repository) { create(:container_image, name: '', project: project) } let(:tag) { repository.tag('tag') } let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } + before do + stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host) + end + it { expect(tag).to respond_to(:repository) } it { expect(tag).to delegate_method(:registry).to(:repository) } it { expect(tag).to delegate_method(:client).to(:repository) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 06617f3b007..9c08f41fe82 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -114,6 +114,8 @@ merge_access_levels: - protected_branch push_access_levels: - protected_branch +container_images: +- name project: - taggings - base_tags @@ -197,6 +199,7 @@ project: - project_authorizations - route - statistics +- container_images award_emoji: - awardable - user diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2dfca8bcfce..83a2efb55b9 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1397,7 +1397,7 @@ describe Ci::Build, :models do { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } end let(:ci_registry_image) do - { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } + { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true } end context 'and is disabled for project' do diff --git a/spec/models/container_image_spec.rb b/spec/models/container_image_spec.rb new file mode 100644 index 00000000000..e0bea737f59 --- /dev/null +++ b/spec/models/container_image_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe ContainerImage do + let(:group) { create(:group, name: 'group') } + let(:project) { create(:project, path: 'test', group: group) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + let(:container_image) { create(:container_image, name: '', project: project, stubbed: false) } + + before do + stub_container_registry_config(enabled: true, api_url: registry_url, host_port: example_host) + stub_request(:get, 'http://example.com/v2/group/test/tags/list'). + with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). + to_return( + status: 200, + body: JSON.dump(tags: ['test']), + headers: { 'Content-Type' => 'application/json' }) + end + + it { expect(container_image).to respond_to(:project) } + it { expect(container_image).to delegate_method(:container_registry).to(:project) } + it { expect(container_image).to delegate_method(:client).to(:container_registry) } + it { expect(container_image.tag('test')).not_to be_nil } + + context '#path' do + subject { container_image.path } + + it { is_expected.to eq('example.com/group/test') } + end + + context 'manifest processing' do + context '#manifest' do + subject { container_image.manifest } + + it { is_expected.not_to be_nil } + end + + context '#valid?' do + subject { container_image.valid? } + + it { is_expected.to be_truthy } + end + + context '#tags' do + subject { container_image.tags } + + it { is_expected.not_to be_empty } + end + end + + context '#delete_tags' do + let(:tag) { ContainerRegistry::Tag.new(container_image, 'tag') } + + before do + expect(container_image).to receive(:tags).twice.and_return([tag]) + expect(tag).to receive(:digest).and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3672a172b088dac5b6d7ad7d49cd620d85cf') + end + + subject { container_image.delete_tags } + + context 'succeeds' do + before { expect(container_image.client).to receive(:delete_repository_tag).and_return(true) } + + it { is_expected.to be_truthy } + end + + context 'any fails' do + before { expect(container_image.client).to receive(:delete_repository_tag).and_return(false) } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 35d932f1c64..aeb4eeb0b55 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -134,18 +134,20 @@ describe Namespace, models: true do expect(@namespace.move_dir).to be_truthy end - context "when any project has container tags" do + context "when any project has container images" do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') - create(:empty_project, namespace: @namespace) + create(:empty_project, namespace: @namespace, container_images: [container_image]) allow(@namespace).to receive(:path_was).and_return(@namespace.path) allow(@namespace).to receive(:path).and_return('new_path') end - it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') } + it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has images in container registry') } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b0087a9e15d..77f2ff3d17b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1173,10 +1173,13 @@ describe Project, models: true do project.rename_repo end - context 'container registry with tags' do + context 'container registry with images' do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') + project.container_images << container_image end subject { project.rename_repo } @@ -1383,20 +1386,20 @@ describe Project, models: true do it { is_expected.to eq(project.path_with_namespace.downcase) } end - describe '#container_registry_repository' do + describe '#container_registry' do let(:project) { create(:empty_project) } before { stub_container_registry_config(enabled: true) } - subject { project.container_registry_repository } + subject { project.container_registry } it { is_expected.not_to be_nil } end - describe '#container_registry_repository_url' do + describe '#container_registry_url' do let(:project) { create(:empty_project) } - subject { project.container_registry_repository_url } + subject { project.container_registry_url } before { stub_container_registry_config(**registry_settings) } @@ -1422,34 +1425,6 @@ describe Project, models: true do end end - describe '#has_container_registry_tags?' do - let(:project) { create(:empty_project) } - - subject { project.has_container_registry_tags? } - - context 'for enabled registry' do - before { stub_container_registry_config(enabled: true) } - - context 'with tags' do - before { stub_container_registry_tags('test', 'test2') } - - it { is_expected.to be_truthy } - end - - context 'when no tags' do - before { stub_container_registry_tags } - - it { is_expected.to be_falsey } - end - end - - context 'for disabled registry' do - before { stub_container_registry_config(enabled: false) } - - it { is_expected.to be_falsey } - end - end - describe '#latest_successful_builds_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 74bfba44dfd..270e630e70e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -90,25 +90,30 @@ describe Projects::DestroyService, services: true do end context 'container registry' do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') + project.container_images << container_image end - context 'tags deletion succeeds' do + context 'images deletion succeeds' do it do - expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) + expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(true) destroy_project(project, user, {}) end end - context 'tags deletion fails' do - before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) } + context 'images deletion fails' do + before do + expect_any_instance_of(ContainerImage).to receive(:delete_tags).and_return(false) + end subject { destroy_project(project, user, {}) } - it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) } + it { expect{subject}.to raise_error(ActiveRecord::RecordNotDestroyed) } end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 5c6fbea8d0e..5e56226ff91 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -29,9 +29,12 @@ describe Projects::TransferService, services: true do end context 'disallow transfering of project with tags' do + let(:container_image) { create(:container_image) } + before do stub_container_registry_config(enabled: true) stub_container_registry_tags('tag') + project.container_images << container_image end subject { transfer_project(project, user, group) } -- cgit v1.2.1 From 164ef8a348cac86097313bc453493ccf739adffe Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Fri, 16 Dec 2016 11:12:37 -0200 Subject: Fixing typos in docs --- doc/administration/container_registry.md | 4 ++-- doc/user/project/container_registry.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 14795601246..4d1cb391e69 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -76,7 +76,7 @@ you modify its settings. Read the upstream documentation on how to achieve that. At the absolute minimum, make sure your [Registry configuration][registry-auth] has `container_registry` as the service and `https://gitlab.example.com/jwt/auth` -as the realm. +as the realm: ``` auth: @@ -494,7 +494,7 @@ configurable in future releases. **GitLab 8.8 ([source docs][8-8-docs])** - GitLab Container Registry feature was introduced. -i + [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure [restart gitlab]: restart_gitlab.md#installations-from-source [wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index eada8e04227..c5b2266ff19 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -10,7 +10,7 @@ - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need to pass a personal access token instead of your password in order to login to GitLab's Container Registry. -- Multiple level image names support was added in GitLab ?8.15? +- Multiple level image names support was added in GitLab 8.15 With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. -- cgit v1.2.1 From b408a192e0fbf630d4f9a4112f6835be50a681d8 Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Fri, 16 Dec 2016 12:07:20 -0200 Subject: Adding mock for full_access_token --- spec/factories/container_images.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/factories/container_images.rb b/spec/factories/container_images.rb index 6141a519a75..3693865101d 100644 --- a/spec/factories/container_images.rb +++ b/spec/factories/container_images.rb @@ -11,6 +11,7 @@ FactoryGirl.define do after(:build) do |image, evaluator| if evaluator.stubbed allow(Gitlab.config.registry).to receive(:enabled).and_return(true) + allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') allow(image.client).to receive(:repository_tags).and_return({ name: image.name_with_namespace, tags: evaluator.tags -- cgit v1.2.1 From ea17df5c4c23890c48cd51af17e2517f04f7c88b Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 25 Jan 2017 10:02:09 -0200 Subject: Fixing minor view issues --- app/views/projects/container_registry/_image.html.haml | 6 +++--- app/views/projects/container_registry/_tag.html.haml | 2 +- app/views/projects/container_registry/index.html.haml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml index 5845efd345a..72f2103b862 100644 --- a/app/views/projects/container_registry/_image.html.haml +++ b/app/views/projects/container_registry/_image.html.haml @@ -8,7 +8,7 @@ = icon("chevron-down") = escape_once(image.name) - = clipboard_button(clipboard_text: "docker pull #{image.path}") + = clipboard_button(clipboard_text: "docker pull #{image.path}") .controls.hidden-xs.pull-right = link_to namespace_project_container_registry_path(@project.namespace, @project, image.id), class: 'btn btn-remove has-tooltip', title: "Remove image", data: { confirm: "Are you sure?" }, method: :delete do = icon("trash cred") @@ -24,8 +24,8 @@ %table.table.tags %thead %tr - %th Name - %th Image ID + %th Tag + %th Tag ID %th Size %th Created - if can?(current_user, :update_container_image, @project) diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index b35a9cb621f..f7161e85428 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -25,5 +25,5 @@ - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Are you sure?" }, method: :delete do + = link_to namespace_project_container_registry_path(@project.namespace, @project, { id: tag.repository.id, tag: tag.name} ), class: 'btn btn-remove has-tooltip', title: "Remove tag", data: { confirm: "Due to a Docker limitation, all tags with the same ID will also be deleted. Are you sure?" }, method: :delete do = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index ab6213f03d8..5508a3de396 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -15,9 +15,9 @@ %br Then you are free to create and upload a container image with build and push commands: %pre - docker build -t #{escape_once(@project.container_registry_url)} . + docker build -t #{escape_once(@project.container_registry_url)}/image . %br - docker push #{escape_once(@project.container_registry_url)} + docker push #{escape_once(@project.container_registry_url)}/image - if @images.blank? .nothing-here-block No container images in Container Registry for this project. -- cgit v1.2.1 From 8294756fc110fdb84036e4ae097940410a8ad6de Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 25 Jan 2017 10:24:50 -0200 Subject: Improved readability in tag/image delete condition --- .../projects/container_registry_controller.rb | 28 +++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb index f656f86fcdb..4981e57ed22 100644 --- a/app/controllers/projects/container_registry_controller.rb +++ b/app/controllers/projects/container_registry_controller.rb @@ -9,31 +9,37 @@ class Projects::ContainerRegistryController < Projects::ApplicationController end def destroy - url = namespace_project_container_registry_index_path(project.namespace, project) - if tag - delete_tag(url) + delete_tag else - if image.destroy - redirect_to url - else - redirect_to url, alert: 'Failed to remove image' - end + delete_image end end private + def registry_url + @registry_url ||= namespace_project_container_registry_index_path(project.namespace, project) + end + def verify_registry_enabled render_404 unless Gitlab.config.registry.enabled end - def delete_tag(url) + def delete_image + if image.destroy + redirect_to registry_url + else + redirect_to registry_url, alert: 'Failed to remove image' + end + end + + def delete_tag if tag.delete image.destroy if image.tags.empty? - redirect_to url + redirect_to registry_url else - redirect_to url, alert: 'Failed to remove tag' + redirect_to registry_url, alert: 'Failed to remove tag' end end -- cgit v1.2.1 From db5b4b8b1a9b8aa07c8310dde53b7c3ed391bafd Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Wed, 22 Feb 2017 11:19:23 -0300 Subject: Creates specs for destroy service and improves namespace container image query performance --- app/models/namespace.rb | 2 +- app/services/container_images/destroy_service.rb | 26 ++--------------- app/views/admin/container_registry/show.html.haml | 2 +- .../20161031013926_create_container_image.rb | 16 ---------- ...egistry_access_token_to_application_settings.rb | 16 ---------- db/schema.rb | 10 +++---- lib/api/registry_events.rb | 4 +-- .../container_images/destroy_service_spec.rb | 34 ++++++++++++++++++++++ 8 files changed, 45 insertions(+), 65 deletions(-) create mode 100644 spec/services/container_images/destroy_service_spec.rb diff --git a/app/models/namespace.rb b/app/models/namespace.rb index c8e329044e0..a803be2e780 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base end def any_project_has_container_registry_images? - projects.any? { |project| project.container_images.present? } + projects.joins(:container_images).any? end def send_update_instructions diff --git a/app/services/container_images/destroy_service.rb b/app/services/container_images/destroy_service.rb index c73b6cfefba..15dca227291 100644 --- a/app/services/container_images/destroy_service.rb +++ b/app/services/container_images/destroy_service.rb @@ -1,31 +1,9 @@ module ContainerImages class DestroyService < BaseService - class DestroyError < StandardError; end - def execute(container_image) - @container_image = container_image - - return false unless can?(current_user, :remove_project, project) - - ContainerImage.transaction do - container_image.destroy! - - unless remove_container_image_tags - raise_error('Failed to remove container image tags. Please try again or contact administrator') - end - end - - true - end - - private - - def raise_error(message) - raise DestroyError.new(message) - end + return false unless can?(current_user, :update_container_image, project) - def remove_container_image_tags - container_image.delete_tags + container_image.destroy! end end end diff --git a/app/views/admin/container_registry/show.html.haml b/app/views/admin/container_registry/show.html.haml index 8803eddda69..ffaa7736d65 100644 --- a/app/views/admin/container_registry/show.html.haml +++ b/app/views/admin/container_registry/show.html.haml @@ -17,7 +17,7 @@ X-Registry-Token: [#{@access_token}] %br Access token is - %code{ id: 'registry-token' } #{@access_token} + %code{ id: 'registry-token' }= @access_token .bs-callout.clearfix .pull-left diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb index 85c0913b8f3..884c78880eb 100644 --- a/db/migrate/20161031013926_create_container_image.rb +++ b/db/migrate/20161031013926_create_container_image.rb @@ -7,22 +7,6 @@ class CreateContainerImage < ActiveRecord::Migration # 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" 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" make sure that this - # method 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 fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change create_table :container_images do |t| t.integer :project_id diff --git a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb index f89f9b00a5f..23d87cc6d0a 100644 --- a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb +++ b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb @@ -7,22 +7,6 @@ class AddContainerRegistryAccessTokenToApplicationSettings < ActiveRecord::Migra # 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" 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" make sure that this - # method 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 fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change add_column :application_settings, :container_registry_access_token, :string end diff --git a/db/schema.rb b/db/schema.rb index 36df20fc8f2..08d11546800 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" + t.integer "max_pages_size", default: 100, null: false t.boolean "require_two_factor_authentication", default: false t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false @@ -107,10 +108,9 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.string "sidekiq_throttling_queues" t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true + t.string "container_registry_access_token" t.string "plantuml_url" t.boolean "plantuml_enabled" - t.string "container_registry_access_token" - t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -586,9 +586,9 @@ ActiveRecord::Schema.define(version: 20170215200045) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree - add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -761,8 +761,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.boolean "lfs_enabled" t.text "description_html" + t.boolean "lfs_enabled" t.integer "parent_id" end @@ -1283,8 +1283,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "organization" t.string "incoming_email_token" + t.string "organization" t.boolean "authorized_projects_populated" t.boolean "notified_of_own_activity", default: false, null: false end diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index 12305a49f0f..fc6fc0b97e0 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -39,9 +39,9 @@ module API params['events'].each do |event| repository = event['target']['repository'] - if event['action'] == 'push' and !!event['target']['tag'] + if event['action'] == 'push' && !!event['target']['tag'] namespace, container_image_name = ContainerImage::split_namespace(repository) - project = Project::find_with_namespace(namespace) + project = Project::find_by_full_path(namespace) if project container_image = project.container_images.find_or_create_by(name: container_image_name) diff --git a/spec/services/container_images/destroy_service_spec.rb b/spec/services/container_images/destroy_service_spec.rb new file mode 100644 index 00000000000..5b4dbaa7934 --- /dev/null +++ b/spec/services/container_images/destroy_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe ContainerImages::DestroyService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:container_image) { create(:container_image, name: '') } + let(:project) { create(:project, path: 'test', namespace: user.namespace, container_images: [container_image]) } + let(:example_host) { 'example.com' } + let(:registry_url) { 'http://' + example_host } + + it { expect(container_image).to be_valid } + it { expect(project.container_images).not_to be_empty } + + context 'when container image has tags' do + before do + project.team << [user, :master] + end + + it 'removes all tags before destroy' do + service = described_class.new(project, user) + + expect(container_image).to receive(:delete_tags).and_return(true) + expect { service.execute(container_image) }.to change(project.container_images, :count).by(-1) + end + + it 'fails when tags are not removed' do + service = described_class.new(project, user) + + expect(container_image).to receive(:delete_tags).and_return(false) + expect { service.execute(container_image) }.to raise_error(ActiveRecord::RecordNotDestroyed) + end + end + end +end -- cgit v1.2.1 From 98a691629d93b2ff3dfbc060b3e25b8ebec6e34b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 29 Nov 2016 13:50:57 +0530 Subject: Hide form inputs for user without access --- app/views/shared/members/_member.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 8e721c9c8dd..a797e8f0799 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -24,6 +24,7 @@ = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray + - expires_soon = member.expires_soon? - if member.request? Requested = time_ago_with_tooltip(member.requested_at) @@ -31,7 +32,7 @@ Joined #{time_ago_with_tooltip(member.created_at)} - if member.expires? · - %span{ class: ('text-warning' if member.expires_soon?) } + %span{ class: "#{"text-warning" if expires_soon} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else @@ -47,7 +48,7 @@ - current_resource = @project || @group .controls.member-controls - if show_controls && member.source == current_resource - - if user != current_user + - if user != current_user && can_admin_member = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 -- cgit v1.2.1 From 3e4dee01740950ae18927aba2deaec63a4ffdc08 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 30 Nov 2016 20:16:32 +0530 Subject: Changelog entry for issue #24861 --- changelogs/unreleased/24861-stringify-group-member-details.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/24861-stringify-group-member-details.yml diff --git a/changelogs/unreleased/24861-stringify-group-member-details.yml b/changelogs/unreleased/24861-stringify-group-member-details.yml new file mode 100644 index 00000000000..a636d43747b --- /dev/null +++ b/changelogs/unreleased/24861-stringify-group-member-details.yml @@ -0,0 +1,4 @@ +--- +title: Hide form inputs for group member without editing rights +merge_request: 7816 +author: Kushal Pandya -- cgit v1.2.1 From 1a36b18122445ebf1014a91cf0b95299bf084ea0 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Dec 2016 11:37:29 +0530 Subject: Remove author name --- changelogs/unreleased/24861-stringify-group-member-details.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/24861-stringify-group-member-details.yml b/changelogs/unreleased/24861-stringify-group-member-details.yml index a636d43747b..f56a1060862 100644 --- a/changelogs/unreleased/24861-stringify-group-member-details.yml +++ b/changelogs/unreleased/24861-stringify-group-member-details.yml @@ -1,4 +1,4 @@ --- title: Hide form inputs for group member without editing rights merge_request: 7816 -author: Kushal Pandya +author: -- cgit v1.2.1 From 7cd1b5104a21ab74f5c1a24fac697e90290ae4d9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 28 Feb 2017 23:05:57 +0530 Subject: Remove unnecessary variable --- app/views/shared/members/_member.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index a797e8f0799..a5aa768b1b2 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -24,7 +24,6 @@ = link_to source.full_name, source, class: "member-group-link" .hidden-xs.cgray - - expires_soon = member.expires_soon? - if member.request? Requested = time_ago_with_tooltip(member.requested_at) @@ -32,7 +31,7 @@ Joined #{time_ago_with_tooltip(member.created_at)} - if member.expires? · - %span{ class: "#{"text-warning" if expires_soon} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } + %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else -- cgit v1.2.1 From a1f5eaf13b55b4110a8fe3823f12266f691c505d Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 01:23:00 -0300 Subject: add writing docs page --- doc/development/writing_documentation.md | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 doc/development/writing_documentation.md diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md new file mode 100644 index 00000000000..39a8204c629 --- /dev/null +++ b/doc/development/writing_documentation.md @@ -0,0 +1,33 @@ +# Writing Documentation + + - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. + - **Documentation Articles**: written by any GitLab Team member, GitLab contributors, or Community Writers. + - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code. + +## Distinction between General Documentation and Documentation Articles + +Every **Documentation Article** contains, in the very beginning, a blockquote with the following information: + +- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial) +- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) +- A reference to the **author's name** and **GitLab.com handle** + +```md +> Type: tutorial +> Level: intermediary +> Author: [Name Surname](https://gitlab.com/username) +``` + +General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, and how to use it or set it up. + +## Documentation Articles - Writing Method + +Use the [writing method](https://about.gitlab.com/handbook/marketing/developer-relations/technical-writing/#writing-method) defined by the Technical Writing team. + +## Documentation Style Guidelines + +All the documentation follow the same [styleguide](https://docs.gitlab.com/ce/development/doc_styleguide.html). + +### Markdown + +Currently GitLab docs use Redcarpet as markdown engine, but there's an open discussion for implementing Kramdown in the near future. -- cgit v1.2.1 From 108a1e99d2c0fcf20e4e68f24a8540e6f3eb1567 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 01:23:58 -0300 Subject: update readme - add doc articles and indexes per topic --- doc/README.md | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/doc/README.md b/doc/README.md index 57d85d770e7..4ee6ba35162 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,10 +1,28 @@ -# GitLab Community Edition documentation +# GitLab Community Edition Documentation -## University +All technical content published by GitLab lives in the documentation, including: -[University](university/README.md) contain guides to learn Git and GitLab through courses and videos. +- **General Documentation** + - [User docs](#user-documentation): general documentation dedicated to regular users of GitLab + - [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances + - [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab +- **Documentation Articles** + - User guides: technical content to guide regular users from point A to point B + - Admin guides: technical content to guide administrators of GitLab instances from point A to point B + - Technical Overviews: technical content describing features, solutions, and third-party integrations + - Tutorials: technical content provided step-by-step on how to do things, or how to reach very specific objectives +- **[Indexes per Topic](topics/)**: content gathering all resources already published by GitLab related to an specific subject or theme, including: + - General Docs + - Documentation Articles + - Blog Posts + - Video Tutorials -## User documentation +See also: + +- [Distinction between General Documentation and Documentation Articles](development/writing_documentation.md#distinction-between-general-documentation-and-documentation-articles) +- GitLab [University](university/README.md): guides to learn Git and GitLab through courses and videos. + +## User Documentation - [Account Security](user/profile/account/two_factor_authentication.md) Securing your account via two-factor authentication, etc. - [API](api/README.md) Automate GitLab via a simple and powerful API. @@ -28,7 +46,7 @@ - [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file. - [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations. -## Administrator documentation +## Administrator Documentation - [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols) Define which Git access protocols can be used to talk to GitLab - [Authentication/Authorization](administration/auth/README.md) Configure @@ -66,7 +84,8 @@ - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability. - [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab. -## Contributor documentation +## Contributor Documentation - [Development](development/README.md) All styleguides and explanations how to contribute. +- [Writing documentation](development/writing_documentation.md) - [Legal](legal/README.md) Contributor license agreements. -- cgit v1.2.1 From c95125e17e9afa3641f1698ebb3dfa9642d331c6 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 01:24:09 -0300 Subject: add intro to indexes per topic --- doc/topics/index.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 doc/topics/index.md diff --git a/doc/topics/index.md b/doc/topics/index.md new file mode 100644 index 00000000000..ef963d26937 --- /dev/null +++ b/doc/topics/index.md @@ -0,0 +1,22 @@ +# Indexes per Topic + +**Indexes per Topic** gather all resources already published by GitLab +related to an specific subject or theme, including: + - General Docs + - Documentation Articles + - Blog Posts + - Video Tutorials + +## Topics + +- [Continuous Integration (GitLab CI)](ci/) +- [GitLab Pages](pages/) +- [Idea to Production](idea-to-production/) + - [GitLab Installation](idea-to-production/installation/) + - [GitLab Workflow](idea-to-production/workflow/) +- [Authentication](authentication/) +- [Deployment from GitLab](deployment/) +- [Integrations](integration/) +- [GitLab Flow (branching strategy)](gitlab-flow/) + +> Note: indexes currently under development. Will be available soon. \ No newline at end of file -- cgit v1.2.1 From cecf7328700626c703f5a5c3ed8f1e7dc831107d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 15 Mar 2017 15:49:16 +0100 Subject: Improve the MR guidelines from the CONTRIBUTING guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CONTRIBUTING.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae143c58290..923cdd8150c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -307,9 +307,12 @@ request is as follows: organized commits by [squashing them][git-squash] 1. Push the commit(s) to your fork 1. Submit a merge request (MR) to the `master` branch -1. Leave the approvals settings as they are: - 1. Your merge request needs at least 1 approval - 1. You don't have to select any approvers + 1. Your merge request needs at least 1 approval but feel free to require more. + For instance if you're touching backend and frontend code, it's a good idea + to require 2 approvals: 1 from a backend maintainer and 1 from a frontend + maintainer + 1. You don't have to select any approvers, but you can if you really want + specific people to approve your merge request 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you used to achieve it. @@ -369,7 +372,7 @@ There are a few rules to get your merge request accepted: 1. If your merge request includes only frontend changes [^1], it must be **approved by a [frontend maintainer][team]**. 1. If your merge request includes frontend and backend changes [^1], it must - be approved by a frontend **and** a backend maintainer. + be **approved by a [frontend and a backend maintainer][team]**. 1. To lower the amount of merge requests maintainers need to review, you can ask or assign any [reviewers][team] for a first review. 1. If you need some guidance (e.g. it's your first merge request), feel free @@ -549,6 +552,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [polling-etag]: https://docs.gitlab.com/ce/development/polling.html -[^1]: Specs other than JavaScript specs are considered backend code. Haml - changes are considered backend code if they include Ruby code other than just - pure HTML. +[^1]: Please note that specs other than JavaScript specs are considered backend + code. -- cgit v1.2.1 From e4cd6c683be59b66778b9612f1cfbe0c3f740eee Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 15:54:23 -0300 Subject: add topics dir to doc_styleguide --- doc/development/doc_styleguide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 9bed441c131..7c0013aa476 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -27,6 +27,7 @@ The table below shows what kind of documentation goes where. | `doc/legal/` | Legal documents about contributing to GitLab. | | `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | | `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | +| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Documentation Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). | --- -- cgit v1.2.1 From dda8896e9382b6e306e8a06bf4bf144f0815c49c Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 15:58:04 -0300 Subject: rename "documentation articles" to "technical articles" --- doc/README.md | 6 +++--- doc/development/doc_styleguide.md | 2 +- doc/development/writing_documentation.md | 6 +++--- doc/topics/index.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/README.md b/doc/README.md index 4ee6ba35162..ab4bdf4903b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -6,20 +6,20 @@ All technical content published by GitLab lives in the documentation, including: - [User docs](#user-documentation): general documentation dedicated to regular users of GitLab - [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances - [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab -- **Documentation Articles** +- **Technical Articles** - User guides: technical content to guide regular users from point A to point B - Admin guides: technical content to guide administrators of GitLab instances from point A to point B - Technical Overviews: technical content describing features, solutions, and third-party integrations - Tutorials: technical content provided step-by-step on how to do things, or how to reach very specific objectives - **[Indexes per Topic](topics/)**: content gathering all resources already published by GitLab related to an specific subject or theme, including: - General Docs - - Documentation Articles + - Technical Articles - Blog Posts - Video Tutorials See also: -- [Distinction between General Documentation and Documentation Articles](development/writing_documentation.md#distinction-between-general-documentation-and-documentation-articles) +- [Distinction between General Documentation and Technical Articles](development/writing_documentation.md#distinction-between-general-documentation-and-technical-articles) - GitLab [University](university/README.md): guides to learn Git and GitLab through courses and videos. ## User Documentation diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 7c0013aa476..1d4dfeea6b1 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -27,7 +27,7 @@ The table below shows what kind of documentation goes where. | `doc/legal/` | Legal documents about contributing to GitLab. | | `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | | `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | -| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Documentation Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). | +| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Technical Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). | --- diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 39a8204c629..c0440604297 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -1,10 +1,10 @@ # Writing Documentation - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - - **Documentation Articles**: written by any GitLab Team member, GitLab contributors, or Community Writers. + - **Technical Articles**: written by any GitLab Team member, GitLab contributors, or Community Writers. - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code. -## Distinction between General Documentation and Documentation Articles +## Distinction between General Documentation and Technical Articles Every **Documentation Article** contains, in the very beginning, a blockquote with the following information: @@ -20,7 +20,7 @@ Every **Documentation Article** contains, in the very beginning, a blockquote wi General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, and how to use it or set it up. -## Documentation Articles - Writing Method +## Technical Articles - Writing Method Use the [writing method](https://about.gitlab.com/handbook/marketing/developer-relations/technical-writing/#writing-method) defined by the Technical Writing team. diff --git a/doc/topics/index.md b/doc/topics/index.md index ef963d26937..c9ad9018b97 100644 --- a/doc/topics/index.md +++ b/doc/topics/index.md @@ -3,7 +3,7 @@ **Indexes per Topic** gather all resources already published by GitLab related to an specific subject or theme, including: - General Docs - - Documentation Articles + - Technical Articles - Blog Posts - Video Tutorials -- cgit v1.2.1 From f6d06ad45747662603a6059b17b2b8b29af1aec9 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 16:00:01 -0300 Subject: rename remaining experession --- doc/development/writing_documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index c0440604297..a9d58c0b1d6 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -6,7 +6,7 @@ ## Distinction between General Documentation and Technical Articles -Every **Documentation Article** contains, in the very beginning, a blockquote with the following information: +Every **Technical Article** contains, in the very beginning, a blockquote with the following information: - A reference to the **type of article** (user guide, admin guide, tech overview, tutorial) - A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) -- cgit v1.2.1 From b3abbe1383db93c17bfd0376cc320efb4236bdd9 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 16:43:22 -0300 Subject: link to tech articles description --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index ab4bdf4903b..1ffe22b3c88 100644 --- a/doc/README.md +++ b/doc/README.md @@ -6,7 +6,7 @@ All technical content published by GitLab lives in the documentation, including: - [User docs](#user-documentation): general documentation dedicated to regular users of GitLab - [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances - [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab -- **Technical Articles** +- **[Technical Articles](development/writing_documentation.md#technical-articles)** - User guides: technical content to guide regular users from point A to point B - Admin guides: technical content to guide administrators of GitLab instances from point A to point B - Technical Overviews: technical content describing features, solutions, and third-party integrations -- cgit v1.2.1 From 2edd3c0fba800006dd4bd9c742bfcc095d8b6877 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 16:43:48 -0300 Subject: describes what a tech article is --- doc/development/writing_documentation.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index a9d58c0b1d6..0a2d6e5891f 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -6,6 +6,29 @@ ## Distinction between General Documentation and Technical Articles +### General Documentation + +General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings. + +### Technical Articles + +Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found. + +They are topic-related documentation, written with an user-friendly approach and language, aim to provide the community with guidance on specific processes to achieve certain objectives. + +A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. + +They live under `doc/topics/topic-name/`, and can be searched per topic, within "Indexes per Topic" pages. The topics are listed on the main [Indexes per Topic](../topics/) page. + +#### Types of Technical Articles + +- **User guides**: technical content to guide regular users from point A to point B +- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B +- **Technical Overviews**: technical content describing features, solutions, and third-party integrations +- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives + +#### Special Format + Every **Technical Article** contains, in the very beginning, a blockquote with the following information: - A reference to the **type of article** (user guide, admin guide, tech overview, tutorial) @@ -18,9 +41,7 @@ Every **Technical Article** contains, in the very beginning, a blockquote with t > Author: [Name Surname](https://gitlab.com/username) ``` -General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, and how to use it or set it up. - -## Technical Articles - Writing Method +#### Technical Articles - Writing Method Use the [writing method](https://about.gitlab.com/handbook/marketing/developer-relations/technical-writing/#writing-method) defined by the Technical Writing team. -- cgit v1.2.1 From 60a09846afafe9f8b8888755892103c7b86e0478 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 16:45:44 -0300 Subject: add distinction between guide and tutorial --- doc/development/writing_documentation.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 0a2d6e5891f..3f658ed58b2 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -27,6 +27,14 @@ They live under `doc/topics/topic-name/`, and can be searched per topic, within - **Technical Overviews**: technical content describing features, solutions, and third-party integrations - **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives +#### Understanding Guides and Tutorials + +Suppose there's a process to go from point A to point B in 5 steps: (A) 1 > 2 > 3 > 4 > 5 (B). + +A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, the step 3, but, assumes that that step is known by the reader, or out of the scope of that article. + +A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. It does not assume that you step 3: it shows you each of them. + #### Special Format Every **Technical Article** contains, in the very beginning, a blockquote with the following information: -- cgit v1.2.1 From fa079afbd89a685b4364079e2cca4c0eefbe5aae Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 16:47:38 -0300 Subject: add link --- doc/development/writing_documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 3f658ed58b2..12a99e0eee3 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -59,4 +59,4 @@ All the documentation follow the same [styleguide](https://docs.gitlab.com/ce/de ### Markdown -Currently GitLab docs use Redcarpet as markdown engine, but there's an open discussion for implementing Kramdown in the near future. +Currently GitLab docs use Redcarpet as markdown engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. -- cgit v1.2.1 From c6a091d63a5bd62004e48c786823459c2307d0bd Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 17:00:00 -0300 Subject: grammar fix --- doc/development/writing_documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 12a99e0eee3..a8cb967b0c7 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -14,7 +14,7 @@ General documentation is categorized by _User_, _Admin_, and _Contributor_, and Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found. -They are topic-related documentation, written with an user-friendly approach and language, aim to provide the community with guidance on specific processes to achieve certain objectives. +They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. -- cgit v1.2.1 From f94b656732b1dbebd634b53fb5ff6ab130a3b638 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 17:13:57 -0300 Subject: add examples of guide vs tutorial --- doc/development/writing_documentation.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index a8cb967b0c7..2aa37d04922 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -29,12 +29,16 @@ They live under `doc/topics/topic-name/`, and can be searched per topic, within #### Understanding Guides and Tutorials -Suppose there's a process to go from point A to point B in 5 steps: (A) 1 > 2 > 3 > 4 > 5 (B). +Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, the step 3, but, assumes that that step is known by the reader, or out of the scope of that article. +- Live example: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.html) to [Part 4](../user/project/pages/getting_started_part_one.html)" + A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. It does not assume that you step 3: it shows you each of them. +- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) + #### Special Format Every **Technical Article** contains, in the very beginning, a blockquote with the following information: -- cgit v1.2.1 From c7b4b150d5db73d453398f01754ea14aaa2254ff Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 16 Mar 2017 17:24:35 -0300 Subject: add the "special format" to the only guide we already have in the docs --- doc/user/project/pages/getting_started_part_four.md | 4 ++++ doc/user/project/pages/getting_started_part_one.md | 4 ++++ doc/user/project/pages/getting_started_part_three.md | 4 ++++ doc/user/project/pages/getting_started_part_two.md | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md index 35af48724f2..50767095aa0 100644 --- a/doc/user/project/pages/getting_started_part_four.md +++ b/doc/user/project/pages/getting_started_part_four.md @@ -1,5 +1,9 @@ # GitLab Pages from A to Z: Part 4 +> **Type**: user guide || +> **Level**: intermediate || +> **Author**: [Marcia Ramos](https://gitlab.com/marcia) + - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 582a4afbab4..e92549aa0df 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -1,5 +1,9 @@ # GitLab Pages from A to Z: Part 1 +> **Type**: user guide || +> **Level**: beginner || +> **Author**: [Marcia Ramos](https://gitlab.com/marcia) + - **Part 1: Static sites and GitLab Pages domains** - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index 55fcd5f00f2..80f16e43e20 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -1,5 +1,9 @@ # GitLab Pages from A to Z: Part 3 +> **Type**: user guide || +> **Level**: beginner || +> **Author**: [Marcia Ramos](https://gitlab.com/marcia) + - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - **Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates** diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md index d0e2c467fee..578ad13f5df 100644 --- a/doc/user/project/pages/getting_started_part_two.md +++ b/doc/user/project/pages/getting_started_part_two.md @@ -1,5 +1,9 @@ # GitLab Pages from A to Z: Part 2 +> **Type**: user guide || +> **Level**: beginner || +> **Author**: [Marcia Ramos](https://gitlab.com/marcia) + - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - **Part 2: Quick start guide - Setting up GitLab Pages** - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) -- cgit v1.2.1 From b1f386841ea0fcf08f33bda90b3dea49dac06c08 Mon Sep 17 00:00:00 2001 From: Matthew Bender Date: Fri, 17 Mar 2017 18:57:45 -0400 Subject: Improves Jira integration documentation --- .../29670-jira-integration-documentation-improvment.yml | 4 ++++ .../project/integrations/img/jira_project_settings.png | Bin 0 -> 32791 bytes doc/user/project/integrations/jira.md | 5 +++++ 3 files changed, 9 insertions(+) create mode 100644 changelogs/unreleased/29670-jira-integration-documentation-improvment.yml create mode 100644 doc/user/project/integrations/img/jira_project_settings.png diff --git a/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml b/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml new file mode 100644 index 00000000000..8975f0b6ef3 --- /dev/null +++ b/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml @@ -0,0 +1,4 @@ +--- +title: Added clarification to the Jira integration documentation. +merge_request: 10066 +author: Matthew Bender diff --git a/doc/user/project/integrations/img/jira_project_settings.png b/doc/user/project/integrations/img/jira_project_settings.png new file mode 100644 index 00000000000..cb6a6ba14ce Binary files /dev/null and b/doc/user/project/integrations/img/jira_project_settings.png differ diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 4c64d1e0907..e02f81fd972 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -157,6 +157,11 @@ the same goal: where `PROJECT-1` is the issue ID of the JIRA project. +>**Note:** +- Only commits and merges into the project's default branch (usually **master**) will + close an issue in Jira. You can change your projects default branch under + [project settings](img/jira_project_settings.png). + ### JIRA issue closing example Let's consider the following example: -- cgit v1.2.1 From 16cca3a0ea7f4b95e99d7b3e8d4953334fa7bec7 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Fri, 17 Mar 2017 17:25:17 +0100 Subject: Expose if action is playable in JSON To avoid a manual build action being played (resulting in a 404), expose `playable?` in the JSON so the frontend can disable/hide the play button if it's not playable. --- app/assets/javascripts/environments/components/environment_item.js | 1 + app/serializers/build_action_entity.rb | 2 ++ app/serializers/build_entity.rb | 1 + spec/serializers/build_action_entity_spec.rb | 4 ++++ spec/serializers/build_entity_spec.rb | 4 ++++ 5 files changed, 12 insertions(+) diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js index 93919d41c60..9d753b4f808 100644 --- a/app/assets/javascripts/environments/components/environment_item.js +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -141,6 +141,7 @@ export default { const parsedAction = { name: gl.text.humanize(action.name), play_path: action.play_path, + playable: action.playable, }; return parsedAction; }); diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb index 184f5fd4b52..184b4b7a681 100644 --- a/app/serializers/build_action_entity.rb +++ b/app/serializers/build_action_entity.rb @@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity build.project, build) end + + expose :playable?, as: :playable end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index 5bcbe285052..2c116102888 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -16,6 +16,7 @@ class BuildEntity < Grape::Entity path_to(:play_namespace_project_build, build) end + expose :playable?, as: :playable expose :created_at expose :updated_at diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb index 0f7be8b2c39..54ac17447b1 100644 --- a/spec/serializers/build_action_entity_spec.rb +++ b/spec/serializers/build_action_entity_spec.rb @@ -17,5 +17,9 @@ describe BuildActionEntity do it 'contains path to the action play' do expect(subject[:path]).to include "builds/#{build.id}/play" end + + it 'contains whether it is playable' do + expect(subject[:playable]).to eq build.playable? + end end end diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index 60c9642ee2c..eed957fa5ef 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -18,6 +18,10 @@ describe BuildEntity do expect(subject).not_to include(/variables/) end + it 'contains whether it is playable' do + expect(subject[:playable]).to eq build.playable? + end + it 'contains timestamps' do expect(subject).to include(:created_at, :updated_at) end -- cgit v1.2.1 From 53d332d3c73f8a883fa54d8eaaf91f92da73c33f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 21 Mar 2017 13:39:57 +0100 Subject: Add configured container registry key to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0b602d613c7..5e9f19d8319 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ eslint-report.html /config/unicorn.rb /config/secrets.yml /config/sidekiq.yml +/config/registry.key /coverage/* /coverage-javascript/ /db/*.sqlite3 -- cgit v1.2.1 From c64d36306cafac463f20d49e750f397a9b32960b Mon Sep 17 00:00:00 2001 From: Andre Guedes Date: Tue, 21 Mar 2017 10:35:02 -0300 Subject: Makes ContainerImages Routable Conflicts: db/schema.rb --- app/models/container_image.rb | 14 ++++++++++++-- .../auth/container_registry_authentication_service.rb | 2 +- app/views/projects/container_registry/_image.html.haml | 3 +-- app/views/projects/container_registry/index.html.haml | 3 +-- db/migrate/20161031013926_create_container_image.rb | 1 + db/schema.rb | 3 ++- lib/api/registry_events.rb | 2 +- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/models/container_image.rb b/app/models/container_image.rb index 583cb977910..a362ea3adbc 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -1,4 +1,6 @@ class ContainerImage < ActiveRecord::Base + include Routable + belongs_to :project delegate :container_registry, :container_registry_allowed_paths, @@ -17,10 +19,18 @@ class ContainerImage < ActiveRecord::Base client.update_token(token) end - def path - [container_registry.path, name_with_namespace].compact.join('/') + def parent + project + end + + def parent_changed? + project_id_changed? end + # def path + # [container_registry.path, name_with_namespace].compact.join('/') + # end + def name_with_namespace [container_registry_path_with_namespace, name].reject(&:blank?).join('/') end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 08fe6e3293e..a3c8d77bf09 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -66,7 +66,7 @@ module Auth # per image authentication. # Removes only last occurence in light # of future nested groups - namespace, _ = ContainerImage::split_namespace(name) + namespace, a = ContainerImage::split_namespace(name) requested_project = Project.find_by_full_path(namespace) return unless requested_project diff --git a/app/views/projects/container_registry/_image.html.haml b/app/views/projects/container_registry/_image.html.haml index 72f2103b862..4fd642a56c9 100644 --- a/app/views/projects/container_registry/_image.html.haml +++ b/app/views/projects/container_registry/_image.html.haml @@ -31,5 +31,4 @@ - if can?(current_user, :update_container_image, @project) %th - - image.tags.each do |tag| - = render 'tag', tag: tag + = render partial: 'tag', collection: image.tags diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml index 5508a3de396..1b5d000e801 100644 --- a/app/views/projects/container_registry/index.html.haml +++ b/app/views/projects/container_registry/index.html.haml @@ -23,5 +23,4 @@ .nothing-here-block No container images in Container Registry for this project. - else - - @images.each do |image| - = render 'image', image: image + = render partial: 'image', collection: @images diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb index 884c78880eb..06c409857da 100644 --- a/db/migrate/20161031013926_create_container_image.rb +++ b/db/migrate/20161031013926_create_container_image.rb @@ -11,6 +11,7 @@ class CreateContainerImage < ActiveRecord::Migration create_table :container_images do |t| t.integer :project_id t.string :name + t.string :path end end end diff --git a/db/schema.rb b/db/schema.rb index db57fb0a548..a1fc5dc1f58 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -108,7 +108,6 @@ ActiveRecord::Schema.define(version: 20170315194013) do t.string "sidekiq_throttling_queues" t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true - t.string "container_registry_access_token" t.string "plantuml_url" t.boolean "plantuml_enabled" t.integer "terminal_max_session_time", default: 0, null: false @@ -117,6 +116,7 @@ ActiveRecord::Schema.define(version: 20170315194013) do t.integer "unique_ips_limit_per_user" t.integer "unique_ips_limit_time_window" t.boolean "unique_ips_limit_enabled", default: false, null: false + t.string "container_registry_access_token" end create_table "audit_events", force: :cascade do |t| @@ -327,6 +327,7 @@ ActiveRecord::Schema.define(version: 20170315194013) do create_table "container_images", force: :cascade do |t| t.integer "project_id" t.string "name" + t.string "path" end create_table "deploy_keys_projects", force: :cascade do |t| diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb index fc6fc0b97e0..8c53e0fcfc0 100644 --- a/lib/api/registry_events.rb +++ b/lib/api/registry_events.rb @@ -44,7 +44,7 @@ module API project = Project::find_by_full_path(namespace) if project - container_image = project.container_images.find_or_create_by(name: container_image_name) + container_image = project.container_images.find_or_create_by(name: container_image_name, path: container_image_name) unless container_image.valid? render_api_error!({ error: "Failed to create container image!" }, 400) -- cgit v1.2.1 From 68a2fa54dedcdbe893ec811413d1703e5f6ac2dc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 22 Mar 2017 11:08:23 +0100 Subject: Remove out-of-scope changes for multi-level images --- .../admin/application_settings_controller.rb | 6 --- .../admin/container_registry_controller.rb | 11 ---- app/models/application_setting.rb | 6 --- app/views/admin/container_registry/show.html.haml | 31 ----------- app/views/admin/dashboard/_head.html.haml | 4 -- config/routes/admin.rb | 2 - ...egistry_access_token_to_application_settings.rb | 13 ----- doc/administration/container_registry.md | 18 ------- lib/api/api.rb | 1 - lib/api/helpers.rb | 10 ---- lib/api/registry_events.rb | 60 ---------------------- lib/container_registry/ROADMAP.md | 7 --- 12 files changed, 169 deletions(-) delete mode 100644 app/controllers/admin/container_registry_controller.rb delete mode 100644 app/views/admin/container_registry/show.html.haml delete mode 100644 db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb delete mode 100644 lib/api/registry_events.rb delete mode 100644 lib/container_registry/ROADMAP.md diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 1d0bd6e0b81..8d831ffdd70 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -29,12 +29,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to :back end - def reset_container_registry_token - @application_setting.reset_container_registry_access_token! - flash[:notice] = 'New container registry access token has been generated!' - redirect_to :back - end - def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async diff --git a/app/controllers/admin/container_registry_controller.rb b/app/controllers/admin/container_registry_controller.rb deleted file mode 100644 index 265c032c67d..00000000000 --- a/app/controllers/admin/container_registry_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Admin::ContainerRegistryController < Admin::ApplicationController - def show - @access_token = container_registry_access_token - end - - private - - def container_registry_access_token - current_application_settings.container_registry_access_token - end -end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9d01a70c77d..671a0fe98cc 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -4,7 +4,6 @@ class ApplicationSetting < ActiveRecord::Base add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token - add_authentication_token_field :container_registry_access_token CACHE_KEY = 'application_setting.last'.freeze DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace @@ -158,7 +157,6 @@ class ApplicationSetting < ActiveRecord::Base before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token - before_save :ensure_container_registry_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -332,10 +330,6 @@ class ApplicationSetting < ActiveRecord::Base ensure_health_check_access_token! end - def container_registry_access_token - ensure_container_registry_access_token! - end - def sidekiq_throttling_enabled? return false unless sidekiq_throttling_column_exists? diff --git a/app/views/admin/container_registry/show.html.haml b/app/views/admin/container_registry/show.html.haml deleted file mode 100644 index ffaa7736d65..00000000000 --- a/app/views/admin/container_registry/show.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -- @no_container = true -= render "admin/dashboard/head" - -%div{ class: container_class } - - %p.prepend-top-default - %span - To properly configure the Container Registry you should add the following - access token to the Docker Registry config.yml as follows: - %pre - %code - :plain - notifications: - endpoints: - - ... - headers: - X-Registry-Token: [#{@access_token}] - %br - Access token is - %code{ id: 'registry-token' }= @access_token - - .bs-callout.clearfix - .pull-left - %p - You can reset container registry access token by pressing the button below. - %p - = button_to reset_container_registry_token_admin_application_settings_path, - method: :put, class: 'btn btn-default', - data: { confirm: 'Are you sure you want to reset container registry token?' } do - = icon('refresh') - Reset container registry access token diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index dbd039547fa..7893c1dee97 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -27,7 +27,3 @@ = link_to admin_runners_path, title: 'Runners' do %span Runners - = nav_link path: 'container_registry#show' do - = link_to admin_container_registry_path, title: 'Registry' do - %span - Registry diff --git a/config/routes/admin.rb b/config/routes/admin.rb index fcbe2e2c435..486ce3c5c87 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -63,7 +63,6 @@ namespace :admin do resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } - resource :container_registry, controller: 'container_registry', only: [:show] resources :projects, only: [:index] @@ -94,7 +93,6 @@ namespace :admin do resources :services, only: [:index, :edit, :update] put :reset_runners_token put :reset_health_check_token - put :reset_container_registry_token put :clear_repository_check_states end diff --git a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb b/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb deleted file mode 100644 index 23d87cc6d0a..00000000000 --- a/db/migrate/20161213212947_add_container_registry_access_token_to_application_settings.rb +++ /dev/null @@ -1,13 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class AddContainerRegistryAccessTokenToApplicationSettings < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # Set this constant to true if this migration requires downtime. - DOWNTIME = false - - def change - add_column :application_settings, :container_registry_access_token, :string - end -end diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index dc4e57f25fb..f707039827b 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -87,23 +87,6 @@ auth: rootcertbundle: /root/certs/certbundle ``` -Also a notification endpoint must be configured with the token from -Admin Area -> Overview -> Registry (`/admin/container_registry`) like in the following sample: - -``` -notifications: - endpoints: - - name: listener - url: https://gitlab.example.com/api/v3/registry_events - headers: - X-Registry-Token: [57Cx95fc2zHFh93VTiGD] - timeout: 500ms - threshold: 5 - backoff: 1s -``` - -Check the [Registry endpoint configuration][registry-endpoint] for details. - ## Container Registry domain configuration There are two ways you can configure the Registry's external domain. @@ -600,7 +583,6 @@ notifications: [storage-config]: https://docs.docker.com/registry/configuration/#storage [registry-http-config]: https://docs.docker.com/registry/configuration/#http [registry-auth]: https://docs.docker.com/registry/configuration/#auth -[registry-endpoint]: https://docs.docker.com/registry/notifications/#/configuration [token-config]: https://docs.docker.com/registry/configuration/#token [8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl diff --git a/lib/api/api.rb b/lib/api/api.rb index 7c7bfada7d0..1bf20f76ad6 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -104,7 +104,6 @@ module API mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings - mount ::API::RegistryEvents mount ::API::Pipelines mount ::API::ProjectHooks mount ::API::Projects diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3c173b544aa..bd22b82476b 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -111,16 +111,6 @@ module API end end - def authenticate_container_registry_access_token! - token = request.headers['X-Registry-Token'] - unless token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare( - token, - current_application_settings.container_registry_access_token - ) - unauthorized! - end - end - def authenticated_as_admin! authenticate! forbidden! unless current_user.is_admin? diff --git a/lib/api/registry_events.rb b/lib/api/registry_events.rb deleted file mode 100644 index 8c53e0fcfc0..00000000000 --- a/lib/api/registry_events.rb +++ /dev/null @@ -1,60 +0,0 @@ -module API - # RegistryEvents API - class RegistryEvents < Grape::API - before { authenticate_container_registry_access_token! } - - content_type :json, 'application/vnd.docker.distribution.events.v1+json' - - params do - requires :events, type: Array, desc: 'The ID of a project' do - requires :id, type: String, desc: 'The ID of the event' - requires :timestamp, type: String, desc: 'Timestamp of the event' - requires :action, type: String, desc: 'Action performed by event' - requires :target, type: Hash, desc: 'Target of the event' do - optional :mediaType, type: String, desc: 'Media type of the target' - optional :size, type: Integer, desc: 'Size in bytes of the target' - requires :digest, type: String, desc: 'Digest of the target' - requires :repository, type: String, desc: 'Repository of target' - optional :url, type: String, desc: 'Url of the target' - optional :tag, type: String, desc: 'Tag of the target' - end - requires :request, type: Hash, desc: 'Request of the event' do - requires :id, type: String, desc: 'The ID of the request' - optional :addr, type: String, desc: 'IP Address of the request client' - optional :host, type: String, desc: 'Hostname of the registry instance' - requires :method, type: String, desc: 'Request method' - requires :useragent, type: String, desc: 'UserAgent header of the request' - end - requires :actor, type: Hash, desc: 'Actor that initiated the event' do - optional :name, type: String, desc: 'Actor name' - end - requires :source, type: Hash, desc: 'Source of the event' do - optional :addr, type: String, desc: 'Hostname of source registry node' - optional :instanceID, type: String, desc: 'Source registry node instanceID' - end - end - end - resource :registry_events do - post do - params['events'].each do |event| - repository = event['target']['repository'] - - if event['action'] == 'push' && !!event['target']['tag'] - namespace, container_image_name = ContainerImage::split_namespace(repository) - project = Project::find_by_full_path(namespace) - - if project - container_image = project.container_images.find_or_create_by(name: container_image_name, path: container_image_name) - - unless container_image.valid? - render_api_error!({ error: "Failed to create container image!" }, 400) - end - else - not_found!('Project') - end - end - end - end - end - end -end diff --git a/lib/container_registry/ROADMAP.md b/lib/container_registry/ROADMAP.md deleted file mode 100644 index e0a20776404..00000000000 --- a/lib/container_registry/ROADMAP.md +++ /dev/null @@ -1,7 +0,0 @@ -## Road map - -### Initial thoughts - -- Determine if image names will be persisted or fetched from API -- If persisted, how to update the stored names upon modification -- If fetched, how to fetch only images of a given project -- cgit v1.2.1 From 29c34267556198ee3dbbe2f13bc81708f5e60f10 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 22 Mar 2017 11:41:05 +0100 Subject: Move container images migration to the present time --- db/migrate/20161031013926_create_container_image.rb | 17 ----------------- db/migrate/20170322013926_create_container_image.rb | 13 +++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 db/migrate/20161031013926_create_container_image.rb create mode 100644 db/migrate/20170322013926_create_container_image.rb diff --git a/db/migrate/20161031013926_create_container_image.rb b/db/migrate/20161031013926_create_container_image.rb deleted file mode 100644 index 06c409857da..00000000000 --- a/db/migrate/20161031013926_create_container_image.rb +++ /dev/null @@ -1,17 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class CreateContainerImage < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # Set this constant to true if this migration requires downtime. - DOWNTIME = false - - def change - create_table :container_images do |t| - t.integer :project_id - t.string :name - t.string :path - end - end -end diff --git a/db/migrate/20170322013926_create_container_image.rb b/db/migrate/20170322013926_create_container_image.rb new file mode 100644 index 00000000000..c494f2a56c7 --- /dev/null +++ b/db/migrate/20170322013926_create_container_image.rb @@ -0,0 +1,13 @@ +class CreateContainerImage < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :container_images do |t| + t.integer :project_id + t.string :name + t.string :path + end + end +end -- cgit v1.2.1 From 95e2c0196b7e492f8c03c6cfeb6b37e97f75813e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 22 Mar 2017 12:28:23 +0100 Subject: Clean code related to accessing registry from project [ci skip] --- app/models/container_image.rb | 28 ++++------------------------ app/models/project.rb | 16 +++++----------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/app/models/container_image.rb b/app/models/container_image.rb index a362ea3adbc..411617ccd71 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -3,36 +3,16 @@ class ContainerImage < ActiveRecord::Base belongs_to :project - delegate :container_registry, :container_registry_allowed_paths, - :container_registry_path_with_namespace, to: :project - + delegate :container_registry, to: :project delegate :client, to: :container_registry validates :manifest, presence: true before_destroy :delete_tags - before_validation :update_token, on: :create - def update_token - paths = container_registry_allowed_paths << name_with_namespace - token = Auth::ContainerRegistryAuthenticationService.full_access_token(paths) - client.update_token(token) - end - - def parent - project - end - - def parent_changed? - project_id_changed? - end - - # def path - # [container_registry.path, name_with_namespace].compact.join('/') - # end - - def name_with_namespace - [container_registry_path_with_namespace, name].reject(&:blank?).join('/') + def registry + # TODO, container registry with image access level + token = Auth::ContainerRegistryAuthenticationService.image_token(self) end def tag(tag) diff --git a/app/models/project.rb b/app/models/project.rb index 928965643a0..4aa9c6bb2f2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -405,29 +405,23 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end - def container_registry_path_with_namespace - path_with_namespace.downcase - end - - def container_registry_allowed_paths - @container_registry_allowed_paths ||= [container_registry_path_with_namespace] + - container_images.map { |i| i.name_with_namespace } - end - def container_registry return unless Gitlab.config.registry.enabled @container_registry ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_allowed_paths) + token = Auth::ContainerRegistryAuthenticationService.full_access_token(project) + url = Gitlab.config.registry.api_url host_port = Gitlab.config.registry.host_port + # TODO, move configuration vars into ContainerRegistry::Registry, clean + # this method up afterwards ContainerRegistry::Registry.new(url, token: token, path: host_port) end end def container_registry_url if Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}" + "#{Gitlab.config.registry.host_port}/#{path_with_namespace.downcase}" end end -- cgit v1.2.1 From e0cc7bd72f3cf99f63950f2d5db27e3221eaeb49 Mon Sep 17 00:00:00 2001 From: Chris Peressini Date: Wed, 22 Mar 2017 11:31:48 +0000 Subject: Update copy for first row in filtered search dropdown --- spec/features/issues/filtered_search/dropdown_hint_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 01b657bcada..bc8cbe30e66 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -43,10 +43,10 @@ describe 'Dropdown hint', js: true, feature: true do end describe 'filtering' do - it 'does not filter `Keep typing and press Enter`' do + it 'does not filter `Press Enter or click to search`' do filtered_search.set('randomtext') - expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) + expect(page).to have_css(js_dropdown_hint, text: 'Press Enter or click to search', visible: false) expect(dropdown_hint_size).to eq(0) end -- cgit v1.2.1 From 67f9614c43070b721993956af1f25fbe4719a345 Mon Sep 17 00:00:00 2001 From: Chris Peressini Date: Wed, 22 Mar 2017 11:41:07 +0000 Subject: Change copy for first row in filtered search --- app/views/shared/issuable/_search_bar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b58640c3ef0..330fa8a5b10 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,7 @@ %button.btn.btn-link = icon('search') %span - Keep typing and press Enter + Press Enter or click to search %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link -- cgit v1.2.1 From 896b13b929369c02f72fa881eda24ca4a6a0d900 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 22 Mar 2017 16:07:27 +0100 Subject: Refactor splitting container image full path [ci skip] --- app/models/container_image.rb | 17 +++++++---------- .../auth/container_registry_authentication_service.rb | 7 +------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/app/models/container_image.rb b/app/models/container_image.rb index 411617ccd71..6e9a060d7a8 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -1,6 +1,4 @@ class ContainerImage < ActiveRecord::Base - include Routable - belongs_to :project delegate :container_registry, to: :project @@ -45,14 +43,13 @@ class ContainerImage < ActiveRecord::Base end end - # rubocop:disable RedundantReturn + def self.from_path(full_path) + return unless full_path.include?('/') - def self.split_namespace(full_path) - image_name = full_path.split('/').last - namespace = full_path.gsub(/(.*)(#{Regexp.escape('/' + image_name)})/, '\1') - if namespace.count('/') < 1 - namespace, image_name = full_path, "" - end - return namespace, image_name + path = full_path[0...full_path.rindex('/')] + name = full_path[full_path.rindex('/')+1..-1] + project = Project.find_by_full_path(path) + + self.new(name: name, path: path, project: project) end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index a3c8d77bf09..7e412040c7c 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -62,12 +62,7 @@ module Auth end def process_repository_access(type, name, actions) - # Strips image name due to lack of - # per image authentication. - # Removes only last occurence in light - # of future nested groups - namespace, a = ContainerImage::split_namespace(name) - requested_project = Project.find_by_full_path(namespace) + requested_project = ContainerImage.from_path(name).project return unless requested_project actions = actions.select do |action| -- cgit v1.2.1 From e1f1f0c3c88a62151d95a0bd17235d35e95f0554 Mon Sep 17 00:00:00 2001 From: alex argunov Date: Wed, 22 Mar 2017 00:06:38 +0300 Subject: Add tooltip to user's calendar activities --- app/views/users/calendar_activities.html.haml | 4 ++-- changelogs/unreleased/calendar-tooltips.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/calendar-tooltips.yml diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 4afd31f788b..d1e88274878 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -18,9 +18,9 @@ = event_action_name(event) %strong - if event.note? - = link_to event.note_target.to_reference, event_note_target_path(event) + = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title - elsif event.target - = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target] + = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title at %strong diff --git a/changelogs/unreleased/calendar-tooltips.yml b/changelogs/unreleased/calendar-tooltips.yml new file mode 100644 index 00000000000..d1517bbab58 --- /dev/null +++ b/changelogs/unreleased/calendar-tooltips.yml @@ -0,0 +1,4 @@ +--- +title: Add tooltip to user's calendar activities +merge_request: 10123 +author: Alex Argunov -- cgit v1.2.1 From bbd5493428efcce72388f08b7feda722aa5e1b2c Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Mar 2017 20:42:54 -0300 Subject: add instructions on subtopics --- doc/development/doc_styleguide.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 1d4dfeea6b1..3c755c192fa 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -57,6 +57,10 @@ The table below shows what kind of documentation goes where. own document located at `doc/user/admin_area/settings/`. For example, the **Visibility and Access Controls** category should have a document located at `doc/user/admin_area/settings/visibility_and_access_controls.md`. +1. The `doc/topics/` directory holds topic-related technical content. Create + `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. + Note that `topics` holds the index page per topic, and techical articles. General + user- and admin- related documentation, should be placed accordingly. --- -- cgit v1.2.1 From 93387599c7dd188dbecb3d21b1da0ffa5e6234b7 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Mar 2017 20:43:15 -0300 Subject: remove linked indexes (examples) --- doc/topics/index.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/doc/topics/index.md b/doc/topics/index.md index c9ad9018b97..43a4ad7422b 100644 --- a/doc/topics/index.md +++ b/doc/topics/index.md @@ -9,14 +9,24 @@ related to an specific subject or theme, including: ## Topics -- [Continuous Integration (GitLab CI)](ci/) +- Idea to Production + - GitLab Installation + - GitLab Workflow + - Chat + - Issue + - Plan + - Code + - Commit + - Test + - Review + - Staging + - Production + - Feedback +- Authentication +- Continuous Integration (GitLab CI) +- GitLab Flow (branching strategy) - [GitLab Pages](pages/) -- [Idea to Production](idea-to-production/) - - [GitLab Installation](idea-to-production/installation/) - - [GitLab Workflow](idea-to-production/workflow/) -- [Authentication](authentication/) -- [Deployment from GitLab](deployment/) -- [Integrations](integration/) -- [GitLab Flow (branching strategy)](gitlab-flow/) +- Integrations -> Note: indexes currently under development. Will be available soon. \ No newline at end of file +> Note: non-linked indexes are currently under development and subjected to change. +More indexes will be available along the time. \ No newline at end of file -- cgit v1.2.1 From e70ed3a1c6be1406e2d53c9886a92686947eabf1 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 22 Mar 2017 20:49:45 -0300 Subject: add pipeline symbols --- doc/development/writing_documentation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 2aa37d04922..194bd26cd24 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -48,8 +48,8 @@ Every **Technical Article** contains, in the very beginning, a blockquote with t - A reference to the **author's name** and **GitLab.com handle** ```md -> Type: tutorial -> Level: intermediary +> Type: tutorial || +> Level: intermediary || > Author: [Name Surname](https://gitlab.com/username) ``` -- cgit v1.2.1 From 7bfa5234855e8de996d5c7dff14ad6c19ea9d33b Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Thu, 23 Mar 2017 11:13:11 +0100 Subject: Resolve future conflicts with CE -> EE merge EE already includes specs for the ApplicationSettingsController, so ensure future changes will be easier to merge to EE. --- .../controllers/admin/application_settings_controller_spec.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 84a1ce773a1..19b35a346b7 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -6,20 +6,23 @@ describe Admin::ApplicationSettingsController do let(:admin) { create(:admin) } before do - sign_in(admin) stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end - describe 'PATCH #update' do + describe 'PUT #update' do + before do + sign_in(admin) + end + it 'updates the default_project_visibility for string value' do - patch :update, application_setting: { default_project_visibility: "20" } + put :update, application_setting: { default_project_visibility: "20" } expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC end it 'falls back to default with default_project_visibility setting is omitted' do - patch :update, application_setting: {} + put :update, application_setting: {} expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE -- cgit v1.2.1 From 4005eb643657e5ee8b1f328e36a3204253e3acf4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 23 Mar 2017 11:41:16 +0100 Subject: Fix communication between GitLab and Container Registry --- app/models/container_image.rb | 23 ++++++++++++++-------- .../container_registry_authentication_service.rb | 17 ++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/models/container_image.rb b/app/models/container_image.rb index 6e9a060d7a8..434302159b0 100644 --- a/app/models/container_image.rb +++ b/app/models/container_image.rb @@ -43,13 +43,20 @@ class ContainerImage < ActiveRecord::Base end end - def self.from_path(full_path) - return unless full_path.include?('/') - - path = full_path[0...full_path.rindex('/')] - name = full_path[full_path.rindex('/')+1..-1] - project = Project.find_by_full_path(path) - - self.new(name: name, path: path, project: project) + def self.project_from_path(image_path) + return unless image_path.include?('/') + + ## + # Projects are always located inside a namespace, so we can remove + # the last node, and see if project with that path exists. + # + truncated_path = image_path.slice(0...image_path.rindex('/')) + + ## + # We still make it possible to search projects by a full image path + # in order to maintain backwards compatibility. + # + Project.find_by_full_path(truncated_path) || + Project.find_by_full_path(image_path) end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 7e412040c7c..2205b0897e2 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -38,13 +38,13 @@ module Auth private def authorized_token(*accesses) - token = JSONWebToken::RSAToken.new(registry.key) - token.issuer = registry.issuer - token.audience = params[:service] - token.subject = current_user.try(:username) - token.expire_time = self.class.token_expire_at - token[:access] = accesses.compact - token + JSONWebToken::RSAToken.new(registry.key).tap do |token| + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token.expire_time = self.class.token_expire_at + token[:access] = accesses.compact + end end def scope @@ -62,7 +62,8 @@ module Auth end def process_repository_access(type, name, actions) - requested_project = ContainerImage.from_path(name).project + requested_project = ContainerImage.project_from_path(name) + return unless requested_project actions = actions.select do |action| -- cgit v1.2.1 From f9ced97a97f5bebec57545fbf611952026bf0d5d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 16 Mar 2017 17:04:58 +0000 Subject: Started iPython notebook viewer Using NotebookLab library [ci skip] --- app/assets/javascripts/blob/notebook_viewer.js | 20 + app/models/blob.rb | 6 + app/views/projects/blob/_notebook.html.haml | 5 + config/webpack.config.js | 2 + vendor/assets/javascripts/notebooklab.js | 2043 ++++++++++++++++++++++++ 5 files changed, 2076 insertions(+) create mode 100644 app/assets/javascripts/blob/notebook_viewer.js create mode 100644 app/views/projects/blob/_notebook.html.haml create mode 100644 vendor/assets/javascripts/notebooklab.js diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js new file mode 100644 index 00000000000..dda137c6603 --- /dev/null +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import NotebookLab from 'vendor/notebooklab'; + +Vue.use(NotebookLab); + +$(() => { + new Vue({ + el: '#js-notebook-viewer', + data() { + return { + json: {}, + }; + }, + template: ` +
+ +
+ `, + }); +}); diff --git a/app/models/blob.rb b/app/models/blob.rb index 1376b86fdad..5b71ac21cc0 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -46,6 +46,10 @@ class Blob < SimpleDelegator text? && language && language.name == 'SVG' end + def ipython_notebook? + text? && language && language.name == 'Jupyter Notebook' + end + def size_within_svg_limits? size <= MAXIMUM_SVG_SIZE end @@ -63,6 +67,8 @@ class Blob < SimpleDelegator end elsif image? || svg? 'image' + elsif ipython_notebook? + 'notebook' elsif text? 'text' else diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/_notebook.html.haml new file mode 100644 index 00000000000..19d3e401d27 --- /dev/null +++ b/app/views/projects/blob/_notebook.html.haml @@ -0,0 +1,5 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('notebook_viewer') + +.file-content#js-notebook-viewer diff --git a/config/webpack.config.js b/config/webpack.config.js index 3cf94b9b435..2ea2b33511b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -37,6 +37,7 @@ var config = { merge_request_widget: './merge_request_widget/ci_bundle.js', monitoring: './monitoring/monitoring_bundle.js', network: './network/network_bundle.js', + notebook_viewer: './blob/notebook_viewer.js', profile: './profile/profile_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js', snippet: './snippet/snippet_bundle.js', @@ -105,6 +106,7 @@ var config = { 'environments_folder', 'issuable', 'merge_conflicts', + 'notebook_viewer', 'vue_pipelines', ], minChunks: function(module, count) { diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js new file mode 100644 index 00000000000..abcd6cd3d98 --- /dev/null +++ b/vendor/assets/javascripts/notebooklab.js @@ -0,0 +1,2043 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("NotebookLab", [], factory); + else if(typeof exports === 'object') + exports["NotebookLab"] = factory(); + else + root["NotebookLab"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; +/******/ +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 19); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +module.exports = function normalizeComponent ( + rawScriptExports, + compiledTemplate, + scopeId, + cssModules +) { + var esModule + var scriptExports = rawScriptExports = rawScriptExports || {} + + // ES6 modules interop + var type = typeof rawScriptExports.default + if (type === 'object' || type === 'function') { + esModule = rawScriptExports + scriptExports = rawScriptExports.default + } + + // Vue.extend constructor export interop + var options = typeof scriptExports === 'function' + ? scriptExports.options + : scriptExports + + // render functions + if (compiledTemplate) { + options.render = compiledTemplate.render + options.staticRenderFns = compiledTemplate.staticRenderFns + } + + // scopedId + if (scopeId) { + options._scopeId = scopeId + } + + // inject cssModules + if (cssModules) { + var computed = Object.create(options.computed || null) + Object.keys(cssModules).forEach(function (key) { + var module = cssModules[key] + computed[key] = function () { return module } + }) + options.computed = computed + } + + return { + esModule: esModule, + exports: scriptExports, + options: options + } +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +var Component = __webpack_require__(0)( + /* script */ + __webpack_require__(6), + /* template */ + __webpack_require__(15), + /* scopeId */ + null, + /* cssModules */ + null +) +Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/prompt.vue" +if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")} +if (Component.options.functional) {console.error("[vue-loader] prompt.vue: functional components are not supported with templates, they should use render functions.")} + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-4f6bf458", Component.options) + } else { + hotAPI.reload("data-v-4f6bf458", Component.options) + } +})()} + +module.exports = Component.exports + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +var Component = __webpack_require__(0)( + /* script */ + __webpack_require__(7), + /* template */ + __webpack_require__(14), + /* scopeId */ + null, + /* cssModules */ + null +) +Component.options.__file = "/Users/phil/Projects/notebooklab/src/index.vue" +if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")} +if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")} + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-4cb2b168", Component.options) + } else { + hotAPI.reload("data-v-4cb2b168", Component.options) + } +})()} + +module.exports = Component.exports + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _index = __webpack_require__(11); + +var _index2 = _interopRequireDefault(_index); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = { + components: { + 'code-cell': _index2.default + }, + props: { + cell: { + type: Object, + required: true + } + }, + computed: { + rawInputCode: function rawInputCode() { + return this.cell.source.join(''); + }, + hasOutput: function hasOutput() { + return this.cell.outputs.length; + }, + output: function output() { + return this.cell.outputs[0]; + }, + outputText: function outputText() { + if (this.output.text) { + return this.output.text.join(''); + } else { + return this.output.data[Object.keys(this.output.data)[0]].join(''); + } + } + } +}; // +// +// +// +// +// +// +// +// +// +// +// +// +// + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _prompt = __webpack_require__(1); + +var _prompt2 = _interopRequireDefault(_prompt); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = { + components: { + 'prompt': _prompt2.default + }, + props: { + count: { + type: Number, + required: false + }, + type: { + type: String, + required: true + }, + rawCode: { + type: String, + required: true + } + }, + computed: { + code: function code() { + if (typeof hljs !== 'undefined' && this.promptType === 'In') { + return hljs.highlightAuto(this.rawCode).value; + } else { + return this.rawCode; + } + }, + promptType: function promptType() { + var type = this.type.split('put')[0]; + + return type.charAt(0).toUpperCase() + type.slice(1);; + } + } +}; // +// +// +// +// +// +// +// +// + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _marked = __webpack_require__(9); + +var _marked2 = _interopRequireDefault(_marked); + +var _prompt = __webpack_require__(1); + +var _prompt2 = _interopRequireDefault(_prompt); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// +// +// +// +// +// +// + +exports.default = { + components: { + 'prompt': _prompt2.default + }, + props: { + cell: { + type: Object, + required: true + } + }, + computed: { + markdown: function markdown() { + var regex = new RegExp(/^\$\$(.*)\$\$$/, 'g'); + + var source = this.cell.source.map(function (line) { + var matches = regex.exec(line.trim()); + + // Only render use the Katex library if it is actually loaded + if (matches && matches.length > 0 && katex) { + return katex.renderToString(matches[1]); + } else { + return line; + } + }); + + return (0, _marked2.default)(source.join('')); + } + } +}; + +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +// +// +// +// +// +// +// +// + +exports.default = { + props: { + type: { + type: String, + required: false + }, + count: { + type: Number, + required: false + } + } +}; + +/***/ }), +/* 7 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _cells = __webpack_require__(8); + +exports.default = { + components: { + 'code-cell': _cells.CodeCell, + 'markdown-cell': _cells.MarkdownCell + }, + props: { + notebook: { + type: Object, + required: true + } + }, + methods: { + cellType: function cellType(type) { + return type + '-cell'; + } + }, + computed: { + hasNotebook: function hasNotebook() { + return Object.keys(this.notebook).length; + } + } +}; // +// +// +// +// +// +// +// +// +// + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _markdown = __webpack_require__(12); + +Object.defineProperty(exports, 'MarkdownCell', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_markdown).default; + } +}); + +var _code = __webpack_require__(10); + +Object.defineProperty(exports, 'CodeCell', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_code).default; + } +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/***/ }), +/* 9 */ +/***/ (function(module, exports, __webpack_require__) { + +/* WEBPACK VAR INJECTION */(function(global) {/** + * marked - a markdown parser + * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) + * https://github.com/chjj/marked + */ + +;(function() { + +/** + * Block-Level Grammar + */ + +var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^( *[-*_]){3,} *(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, + nptable: noop, + lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, + blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, + list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, + def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, + table: noop, + paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, + text: /^[^\n]+/ +}; + +block.bullet = /(?:[*+-]|\d+\.)/; +block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; +block.item = replace(block.item, 'gm') + (/bull/g, block.bullet) + (); + +block.list = replace(block.list) + (/bull/g, block.bullet) + ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') + ('def', '\\n+(?=' + block.def.source + ')') + (); + +block.blockquote = replace(block.blockquote) + ('def', block.def) + (); + +block._tag = '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b'; + +block.html = replace(block.html) + ('comment', //) + ('closed', /<(tag)[\s\S]+?<\/\1>/) + ('closing', /])*?>/) + (/tag/g, block._tag) + (); + +block.paragraph = replace(block.paragraph) + ('hr', block.hr) + ('heading', block.heading) + ('lheading', block.lheading) + ('blockquote', block.blockquote) + ('tag', '<' + block._tag) + ('def', block.def) + (); + +/** + * Normal Block Grammar + */ + +block.normal = merge({}, block); + +/** + * GFM Block Grammar + */ + +block.gfm = merge({}, block.normal, { + fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/, + paragraph: /^/, + heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ +}); + +block.gfm.paragraph = replace(block.paragraph) + ('(?!', '(?!' + + block.gfm.fences.source.replace('\\1', '\\2') + '|' + + block.list.source.replace('\\1', '\\3') + '|') + (); + +/** + * GFM + Tables Block Grammar + */ + +block.tables = merge({}, block.gfm, { + nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, + table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ +}); + +/** + * Block Lexer + */ + +function Lexer(options) { + this.tokens = []; + this.tokens.links = {}; + this.options = options || marked.defaults; + this.rules = block.normal; + + if (this.options.gfm) { + if (this.options.tables) { + this.rules = block.tables; + } else { + this.rules = block.gfm; + } + } +} + +/** + * Expose Block Rules + */ + +Lexer.rules = block; + +/** + * Static Lex Method + */ + +Lexer.lex = function(src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); +}; + +/** + * Preprocessing + */ + +Lexer.prototype.lex = function(src) { + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\u00a0/g, ' ') + .replace(/\u2424/g, '\n'); + + return this.token(src, true); +}; + +/** + * Lexing + */ + +Lexer.prototype.token = function(src, top, bq) { + var src = src.replace(/^ +$/gm, '') + , next + , loose + , cap + , bull + , b + , item + , space + , i + , l; + + while (src) { + // newline + if (cap = this.rules.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + this.tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + this.tokens.push({ + type: 'code', + text: !this.options.pedantic + ? cap.replace(/\n+$/, '') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = this.rules.fences.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'code', + lang: cap[2], + text: cap[3] || '' + }); + continue; + } + + // heading + if (cap = this.rules.heading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // table no leading pipe (gfm) + if (top && (cap = this.rules.nptable.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: 'table', + header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3].replace(/\n$/, '').split('\n') + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i].split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // lheading + if (cap = this.rules.lheading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // hr + if (cap = this.rules.hr.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = this.rules.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: 'blockquote_start' + }); + + cap = cap[0].replace(/^ *> ?/gm, ''); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + this.token(cap, top, true); + + this.tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = this.rules.list.exec(src)) { + src = src.substring(cap[0].length); + bull = cap[2]; + + this.tokens.push({ + type: 'list_start', + ordered: bull.length > 1 + }); + + // Get each top-level item. + cap = cap[0].match(this.rules.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !this.options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (this.options.smartLists && i !== l - 1) { + b = block.bullet.exec(cap[i + 1])[0]; + if (bull !== b && !(bull.length > 1 && b.length > 1)) { + src = cap.slice(i + 1).join('\n') + src; + i = l - 1; + } + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item.charAt(item.length - 1) === '\n'; + if (!loose) loose = next; + } + + this.tokens.push({ + type: loose + ? 'loose_item_start' + : 'list_item_start' + }); + + // Recurse. + this.token(item, false, bq); + + this.tokens.push({ + type: 'list_item_end' + }); + } + + this.tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = this.rules.html.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: this.options.sanitize + ? 'paragraph' + : 'html', + pre: !this.options.sanitizer + && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), + text: cap[0] + }); + continue; + } + + // def + if ((!bq && top) && (cap = this.rules.def.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.links[cap[1].toLowerCase()] = { + href: cap[2], + title: cap[3] + }; + continue; + } + + // table (gfm) + if (top && (cap = this.rules.table.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: 'table', + header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i] + .replace(/^ *\| *| *\| *$/g, '') + .split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // top-level paragraph + if (top && (cap = this.rules.paragraph.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'paragraph', + text: cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1] + }); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + + if (src) { + throw new + Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return this.tokens; +}; + +/** + * Inline-Level Grammar + */ + +var inline = { + escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, + autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, + url: noop, + tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, + link: /^!?\[(inside)\]\(href\)/, + reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, + nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, + strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, + em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, + code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + del: noop, + text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; + +inline.link = replace(inline.link) + ('inside', inline._inside) + ('href', inline._href) + (); + +inline.reflink = replace(inline.reflink) + ('inside', inline._inside) + (); + +/** + * Normal Inline Grammar + */ + +inline.normal = merge({}, inline); + +/** + * Pedantic Inline Grammar + */ + +inline.pedantic = merge({}, inline.normal, { + strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ +}); + +/** + * GFM Inline Grammar + */ + +inline.gfm = merge({}, inline.normal, { + escape: replace(inline.escape)('])', '~|])')(), + url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, + del: /^~~(?=\S)([\s\S]*?\S)~~/, + text: replace(inline.text) + (']|', '~]|') + ('|', '|https?://|') + () +}); + +/** + * GFM + Line Breaks Inline Grammar + */ + +inline.breaks = merge({}, inline.gfm, { + br: replace(inline.br)('{2,}', '*')(), + text: replace(inline.gfm.text)('{2,}', '*')() +}); + +/** + * Inline Lexer & Compiler + */ + +function InlineLexer(links, options) { + this.options = options || marked.defaults; + this.links = links; + this.rules = inline.normal; + this.renderer = this.options.renderer || new Renderer; + this.renderer.options = this.options; + + if (!this.links) { + throw new + Error('Tokens array requires a `links` property.'); + } + + if (this.options.gfm) { + if (this.options.breaks) { + this.rules = inline.breaks; + } else { + this.rules = inline.gfm; + } + } else if (this.options.pedantic) { + this.rules = inline.pedantic; + } +} + +/** + * Expose Inline Rules + */ + +InlineLexer.rules = inline; + +/** + * Static Lexing/Compiling Method + */ + +InlineLexer.output = function(src, links, options) { + var inline = new InlineLexer(links, options); + return inline.output(src); +}; + +/** + * Lexing/Compiling + */ + +InlineLexer.prototype.output = function(src) { + var out = '' + , link + , text + , href + , cap; + + while (src) { + // escape + if (cap = this.rules.escape.exec(src)) { + src = src.substring(cap[0].length); + out += cap[1]; + continue; + } + + // autolink + if (cap = this.rules.autolink.exec(src)) { + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = cap[1].charAt(6) === ':' + ? this.mangle(cap[1].substring(7)) + : this.mangle(cap[1]); + href = this.mangle('mailto:') + text; + } else { + text = escape(cap[1]); + href = text; + } + out += this.renderer.link(href, null, text); + continue; + } + + // url (gfm) + if (!this.inLink && (cap = this.rules.url.exec(src))) { + src = src.substring(cap[0].length); + text = escape(cap[1]); + href = text; + out += this.renderer.link(href, null, text); + continue; + } + + // tag + if (cap = this.rules.tag.exec(src)) { + if (!this.inLink && /^/i.test(cap[0])) { + this.inLink = false; + } + src = src.substring(cap[0].length); + out += this.options.sanitize + ? this.options.sanitizer + ? this.options.sanitizer(cap[0]) + : escape(cap[0]) + : cap[0] + continue; + } + + // link + if (cap = this.rules.link.exec(src)) { + src = src.substring(cap[0].length); + this.inLink = true; + out += this.outputLink(cap, { + href: cap[2], + title: cap[3] + }); + this.inLink = false; + continue; + } + + // reflink, nolink + if ((cap = this.rules.reflink.exec(src)) + || (cap = this.rules.nolink.exec(src))) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = this.links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0].charAt(0); + src = cap[0].substring(1) + src; + continue; + } + this.inLink = true; + out += this.outputLink(cap, link); + this.inLink = false; + continue; + } + + // strong + if (cap = this.rules.strong.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.strong(this.output(cap[2] || cap[1])); + continue; + } + + // em + if (cap = this.rules.em.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.em(this.output(cap[2] || cap[1])); + continue; + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.codespan(escape(cap[2], true)); + continue; + } + + // br + if (cap = this.rules.br.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.br(); + continue; + } + + // del (gfm) + if (cap = this.rules.del.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.del(this.output(cap[1])); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.text(escape(this.smartypants(cap[0]))); + continue; + } + + if (src) { + throw new + Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return out; +}; + +/** + * Compile Link + */ + +InlineLexer.prototype.outputLink = function(cap, link) { + var href = escape(link.href) + , title = link.title ? escape(link.title) : null; + + return cap[0].charAt(0) !== '!' + ? this.renderer.link(href, title, this.output(cap[1])) + : this.renderer.image(href, title, escape(cap[1])); +}; + +/** + * Smartypants Transformations + */ + +InlineLexer.prototype.smartypants = function(text) { + if (!this.options.smartypants) return text; + return text + // em-dashes + .replace(/---/g, '\u2014') + // en-dashes + .replace(/--/g, '\u2013') + // opening singles + .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') + // closing singles & apostrophes + .replace(/'/g, '\u2019') + // opening doubles + .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') + // closing doubles + .replace(/"/g, '\u201d') + // ellipses + .replace(/\.{3}/g, '\u2026'); +}; + +/** + * Mangle Links + */ + +InlineLexer.prototype.mangle = function(text) { + if (!this.options.mangle) return text; + var out = '' + , l = text.length + , i = 0 + , ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +}; + +/** + * Renderer + */ + +function Renderer(options) { + this.options = options || {}; +} + +Renderer.prototype.code = function(code, lang, escaped) { + if (this.options.highlight) { + var out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + if (!lang) { + return '
'
+      + (escaped ? code : escape(code, true))
+      + '\n
'; + } + + return '
'
+    + (escaped ? code : escape(code, true))
+    + '\n
\n'; +}; + +Renderer.prototype.blockquote = function(quote) { + return '
\n' + quote + '
\n'; +}; + +Renderer.prototype.html = function(html) { + return html; +}; + +Renderer.prototype.heading = function(text, level, raw) { + return '' + + text + + '\n'; +}; + +Renderer.prototype.hr = function() { + return this.options.xhtml ? '
\n' : '
\n'; +}; + +Renderer.prototype.list = function(body, ordered) { + var type = ordered ? 'ol' : 'ul'; + return '<' + type + '>\n' + body + '\n'; +}; + +Renderer.prototype.listitem = function(text) { + return '
  • ' + text + '
  • \n'; +}; + +Renderer.prototype.paragraph = function(text) { + return '

    ' + text + '

    \n'; +}; + +Renderer.prototype.table = function(header, body) { + return '\n' + + '\n' + + header + + '\n' + + '\n' + + body + + '\n' + + '
    \n'; +}; + +Renderer.prototype.tablerow = function(content) { + return '\n' + content + '\n'; +}; + +Renderer.prototype.tablecell = function(content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align + ? '<' + type + ' style="text-align:' + flags.align + '">' + : '<' + type + '>'; + return tag + content + '\n'; +}; + +// span level renderer +Renderer.prototype.strong = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.em = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.codespan = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.br = function() { + return this.options.xhtml ? '
    ' : '
    '; +}; + +Renderer.prototype.del = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.link = function(href, title, text) { + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return ''; + } + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) { + return ''; + } + } + var out = '
    '; + return out; +}; + +Renderer.prototype.image = function(href, title, text) { + var out = '' + text + '' : '>'; + return out; +}; + +Renderer.prototype.text = function(text) { + return text; +}; + +/** + * Parsing & Compiling + */ + +function Parser(options) { + this.tokens = []; + this.token = null; + this.options = options || marked.defaults; + this.options.renderer = this.options.renderer || new Renderer; + this.renderer = this.options.renderer; + this.renderer.options = this.options; +} + +/** + * Static Parse Method + */ + +Parser.parse = function(src, options, renderer) { + var parser = new Parser(options, renderer); + return parser.parse(src); +}; + +/** + * Parse Loop + */ + +Parser.prototype.parse = function(src) { + this.inline = new InlineLexer(src.links, this.options, this.renderer); + this.tokens = src.reverse(); + + var out = ''; + while (this.next()) { + out += this.tok(); + } + + return out; +}; + +/** + * Next Token + */ + +Parser.prototype.next = function() { + return this.token = this.tokens.pop(); +}; + +/** + * Preview Next Token + */ + +Parser.prototype.peek = function() { + return this.tokens[this.tokens.length - 1] || 0; +}; + +/** + * Parse Text Tokens + */ + +Parser.prototype.parseText = function() { + var body = this.token.text; + + while (this.peek().type === 'text') { + body += '\n' + this.next().text; + } + + return this.inline.output(body); +}; + +/** + * Parse Current Token + */ + +Parser.prototype.tok = function() { + switch (this.token.type) { + case 'space': { + return ''; + } + case 'hr': { + return this.renderer.hr(); + } + case 'heading': { + return this.renderer.heading( + this.inline.output(this.token.text), + this.token.depth, + this.token.text); + } + case 'code': { + return this.renderer.code(this.token.text, + this.token.lang, + this.token.escaped); + } + case 'table': { + var header = '' + , body = '' + , i + , row + , cell + , flags + , j; + + // header + cell = ''; + for (i = 0; i < this.token.header.length; i++) { + flags = { header: true, align: this.token.align[i] }; + cell += this.renderer.tablecell( + this.inline.output(this.token.header[i]), + { header: true, align: this.token.align[i] } + ); + } + header += this.renderer.tablerow(cell); + + for (i = 0; i < this.token.cells.length; i++) { + row = this.token.cells[i]; + + cell = ''; + for (j = 0; j < row.length; j++) { + cell += this.renderer.tablecell( + this.inline.output(row[j]), + { header: false, align: this.token.align[j] } + ); + } + + body += this.renderer.tablerow(cell); + } + return this.renderer.table(header, body); + } + case 'blockquote_start': { + var body = ''; + + while (this.next().type !== 'blockquote_end') { + body += this.tok(); + } + + return this.renderer.blockquote(body); + } + case 'list_start': { + var body = '' + , ordered = this.token.ordered; + + while (this.next().type !== 'list_end') { + body += this.tok(); + } + + return this.renderer.list(body, ordered); + } + case 'list_item_start': { + var body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.token.type === 'text' + ? this.parseText() + : this.tok(); + } + + return this.renderer.listitem(body); + } + case 'loose_item_start': { + var body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.tok(); + } + + return this.renderer.listitem(body); + } + case 'html': { + var html = !this.token.pre && !this.options.pedantic + ? this.inline.output(this.token.text) + : this.token.text; + return this.renderer.html(html); + } + case 'paragraph': { + return this.renderer.paragraph(this.inline.output(this.token.text)); + } + case 'text': { + return this.renderer.paragraph(this.parseText()); + } + } +}; + +/** + * Helpers + */ + +function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + +function replace(regex, opt) { + regex = regex.source; + opt = opt || ''; + return function self(name, val) { + if (!name) return new RegExp(regex, opt); + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, '$1'); + regex = regex.replace(name, val); + return self; + }; +} + +function noop() {} +noop.exec = noop; + +function merge(obj) { + var i = 1 + , target + , key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; +} + + +/** + * Marked + */ + +function marked(src, opt, callback) { + if (callback || typeof opt === 'function') { + if (!callback) { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + + var highlight = opt.highlight + , tokens + , pending + , i = 0; + + try { + tokens = Lexer.lex(src, opt) + } catch (e) { + return callback(e); + } + + pending = tokens.length; + + var done = function(err) { + if (err) { + opt.highlight = highlight; + return callback(err); + } + + var out; + + try { + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + + opt.highlight = highlight; + + return err + ? callback(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!pending) return done(); + + for (; i < tokens.length; i++) { + (function(token) { + if (token.type !== 'code') { + return --pending || done(); + } + return highlight(token.text, token.lang, function(err, code) { + if (err) return done(err); + if (code == null || code === token.text) { + return --pending || done(); + } + token.text = code; + token.escaped = true; + --pending || done(); + }); + })(tokens[i]); + } + + return; + } + try { + if (opt) opt = merge({}, marked.defaults, opt); + return Parser.parse(Lexer.lex(src, opt), opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/chjj/marked.'; + if ((opt || marked.defaults).silent) { + return '

    An error occured:

    '
    +        + escape(e.message + '', true)
    +        + '
    '; + } + throw e; + } +} + +/** + * Options + */ + +marked.options = +marked.setOptions = function(opt) { + merge(marked.defaults, opt); + return marked; +}; + +marked.defaults = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: false, + sanitizer: null, + mangle: true, + smartLists: false, + silent: false, + highlight: null, + langPrefix: 'lang-', + smartypants: false, + headerPrefix: '', + renderer: new Renderer, + xhtml: false +}; + +/** + * Expose + */ + +marked.Parser = Parser; +marked.parser = Parser.parse; + +marked.Renderer = Renderer; + +marked.Lexer = Lexer; +marked.lexer = Lexer.lex; + +marked.InlineLexer = InlineLexer; +marked.inlineLexer = InlineLexer.output; + +marked.parse = marked; + +if (true) { + module.exports = marked; +} else if (typeof define === 'function' && define.amd) { + define(function() { return marked; }); +} else { + this.marked = marked; +} + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}()); + +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(18))) + +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + +var Component = __webpack_require__(0)( + /* script */ + __webpack_require__(3), + /* template */ + __webpack_require__(13), + /* scopeId */ + null, + /* cssModules */ + null +) +Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code.vue" +if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")} +if (Component.options.functional) {console.error("[vue-loader] code.vue: functional components are not supported with templates, they should use render functions.")} + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-3ac4c361", Component.options) + } else { + hotAPI.reload("data-v-3ac4c361", Component.options) + } +})()} + +module.exports = Component.exports + + +/***/ }), +/* 11 */ +/***/ (function(module, exports, __webpack_require__) { + +var Component = __webpack_require__(0)( + /* script */ + __webpack_require__(4), + /* template */ + __webpack_require__(17), + /* scopeId */ + null, + /* cssModules */ + null +) +Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code/index.vue" +if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")} +if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")} + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-d42105b8", Component.options) + } else { + hotAPI.reload("data-v-d42105b8", Component.options) + } +})()} + +module.exports = Component.exports + + +/***/ }), +/* 12 */ +/***/ (function(module, exports, __webpack_require__) { + +var Component = __webpack_require__(0)( + /* script */ + __webpack_require__(5), + /* template */ + __webpack_require__(16), + /* scopeId */ + null, + /* cssModules */ + null +) +Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/markdown.vue" +if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")} +if (Component.options.functional) {console.error("[vue-loader] markdown.vue: functional components are not supported with templates, they should use render functions.")} + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-7342b363", Component.options) + } else { + hotAPI.reload("data-v-7342b363", Component.options) + } +})()} + +module.exports = Component.exports + + +/***/ }), +/* 13 */ +/***/ (function(module, exports, __webpack_require__) { + +module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; + return _c('div', { + staticClass: "cell" + }, [_c('code-cell', { + attrs: { + "type": "input", + "raw-code": _vm.rawInputCode, + "count": _vm.cell.execution_count + } + }), _vm._v(" "), (_vm.hasOutput) ? _c('code-cell', { + attrs: { + "type": "output", + "raw-code": _vm.outputText, + "count": _vm.output.execution_count + } + }) : _vm._e()], 1) +},staticRenderFns: []} +module.exports.render._withStripped = true +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-3ac4c361", module.exports) + } +} + +/***/ }), +/* 14 */ +/***/ (function(module, exports, __webpack_require__) { + +module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; + return (_vm.hasNotebook) ? _c('div', _vm._l((_vm.notebook.cells), function(cell, index) { + return _c(_vm.cellType(cell.cell_type), { + key: index, + tag: "component", + attrs: { + "cell": cell + } + }) + })) : _vm._e() +},staticRenderFns: []} +module.exports.render._withStripped = true +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-4cb2b168", module.exports) + } +} + +/***/ }), +/* 15 */ +/***/ (function(module, exports, __webpack_require__) { + +module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; + return _c('div', { + staticClass: "prompt" + }, [(_vm.type && _vm.count) ? _c('span', [_vm._v("\n " + _vm._s(_vm.type) + " [" + _vm._s(_vm.count) + "]:\n ")]) : _vm._e()]) +},staticRenderFns: []} +module.exports.render._withStripped = true +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-4f6bf458", module.exports) + } +} + +/***/ }), +/* 16 */ +/***/ (function(module, exports, __webpack_require__) { + +module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; + return _c('div', { + staticClass: "cell text-cell" + }, [_c('prompt'), _vm._v(" "), _c('div', { + staticClass: "markdown", + domProps: { + "innerHTML": _vm._s(_vm.markdown) + } + })], 1) +},staticRenderFns: []} +module.exports.render._withStripped = true +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-7342b363", module.exports) + } +} + +/***/ }), +/* 17 */ +/***/ (function(module, exports, __webpack_require__) { + +module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; + return _c('div', { + class: _vm.type + }, [_c('prompt', { + attrs: { + "type": _vm.promptType, + "count": _vm.count + } + }), _vm._v(" "), _c('pre', { + domProps: { + "innerHTML": _vm._s(_vm.code) + } + })], 1) +},staticRenderFns: []} +module.exports.render._withStripped = true +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-d42105b8", module.exports) + } +} + +/***/ }), +/* 18 */ +/***/ (function(module, exports) { + +var g; + +// This works in non-strict mode +g = (function() { + return this; +})(); + +try { + // This works if eval is allowed (see CSP) + g = g || Function("return this")() || (1,eval)("this"); +} catch(e) { + // This works if the window reference is available + if(typeof window === "object") + g = window; +} + +// g can still be undefined, but nothing to do about it... +// We return undefined, instead of nothing here, so it's +// easier to handle this case. if(!global) { ...} + +module.exports = g; + + +/***/ }), +/* 19 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var Notebook = __webpack_require__(2); + +module.exports = { + install: function install(_vue) { + _vue.component('notebook-lab', Notebook); + } +}; + +/***/ }) +/******/ ]); +}); \ No newline at end of file -- cgit v1.2.1 From 33f26de56bca2c5c2d6a4da29ed2217b8abf1710 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 17 Mar 2017 17:00:55 +0000 Subject: Loads raw JSON & displays in the component [ci skip] --- app/assets/javascripts/blob/notebook_viewer.js | 35 +++++++++++++++++++++++--- app/assets/stylesheets/application.scss | 1 + app/views/projects/blob/_notebook.html.haml | 2 +- vendor/assets/stylesheets/notebooklab.css | 27 ++++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 vendor/assets/stylesheets/notebooklab.css diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js index dda137c6603..057afc9b2e5 100644 --- a/app/assets/javascripts/blob/notebook_viewer.js +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -1,20 +1,49 @@ import Vue from 'vue'; +import VueResource from 'vue-resource'; import NotebookLab from 'vendor/notebooklab'; +Vue.use(VueResource); Vue.use(NotebookLab); $(() => { + const el = document.getElementById('js-notebook-viewer'); + new Vue({ - el: '#js-notebook-viewer', + el, data() { return { + loading: true, json: {}, }; }, template: ` -
    - +
    + + +
    `, + mounted() { + $.get(gon.katex_css_url, () => { + const css = $('', { + rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }); + css.appendTo('head'); + }); + + $.getScript(gon.katex_js_url, () => { + this.$http.get(el.dataset.endpoint) + .then((res) => { + this.json = res.json(); + this.loading = false; + }); + }); + }, }); }); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 83a8eeaafde..0a4c69bc5ac 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -7,6 +7,7 @@ *= require_self *= require dropzone/basic *= require cropper.css + *= require notebooklab.css */ /* diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/_notebook.html.haml index 19d3e401d27..ab1cf933944 100644 --- a/app/views/projects/blob/_notebook.html.haml +++ b/app/views/projects/blob/_notebook.html.haml @@ -2,4 +2,4 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('notebook_viewer') -.file-content#js-notebook-viewer +.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } } diff --git a/vendor/assets/stylesheets/notebooklab.css b/vendor/assets/stylesheets/notebooklab.css new file mode 100644 index 00000000000..0caf729a17f --- /dev/null +++ b/vendor/assets/stylesheets/notebooklab.css @@ -0,0 +1,27 @@ +.cell, +.input, +.output { + display: flex; + width: 100%; + margin-bottom: 10px; +} + +.cell:not(.text-cell) { + flex-direction: column; +} + +.prompt { + padding: 0 10px; + min-width: 7em; + font-family: monospace; +} + +.cell pre { + margin: 0; + width: 100%; +} + +.markdown .katex { + display: block; + text-align: center; +} -- cgit v1.2.1 From 313258fa423f9c3a03e793e2a7bc8686d0757e91 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 20 Mar 2017 14:44:07 +0000 Subject: Stops errors in the rendering breaking the page Fixed some output types [ci skip] --- app/assets/javascripts/blob/notebook_viewer.js | 18 ++++++----- vendor/assets/javascripts/notebooklab.js | 41 ++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js index 057afc9b2e5..4bd80393d17 100644 --- a/app/assets/javascripts/blob/notebook_viewer.js +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -5,6 +5,10 @@ import NotebookLab from 'vendor/notebooklab'; Vue.use(VueResource); Vue.use(NotebookLab); +Vue.config.errorHandler = (err) => { + console.log(err); +} + $(() => { const el = document.getElementById('js-notebook-viewer'); @@ -12,6 +16,7 @@ $(() => { el, data() { return { + error: false, loading: true, json: {}, }; @@ -28,14 +33,11 @@ $(() => {
    `, mounted() { - $.get(gon.katex_css_url, () => { - const css = $('', { - rel: 'stylesheet', - type: 'text/css', - href: gon.katex_css_url, - }); - css.appendTo('head'); - }); + $('', { + rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }).appendTo('head'); $.getScript(gon.katex_js_url, () => { this.$http.get(el.dataset.endpoint) diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js index abcd6cd3d98..06d41f03211 100644 --- a/vendor/assets/javascripts/notebooklab.js +++ b/vendor/assets/javascripts/notebooklab.js @@ -235,11 +235,24 @@ exports.default = { output: function output() { return this.cell.outputs[0]; }, + outputType: function outputType() { + if (!this.output.text) { + return Object.keys(this.output.data)[0]; + } + }, outputText: function outputText() { if (this.output.text) { return this.output.text.join(''); } else { - return this.output.data[Object.keys(this.output.data)[0]].join(''); + var output = this.output.data[this.outputType]; + + if (typeof output === 'array') { + return output.join(''); + } else if (typeof output === 'string') { + return output; + } else { + return ''; + } } } } @@ -257,6 +270,7 @@ exports.default = { // // // +// /***/ }), /* 4 */ @@ -284,6 +298,11 @@ exports.default = { type: Number, required: false }, + outputType: { + type: String, + required: false, + default: '' + }, type: { type: String, required: true @@ -305,6 +324,13 @@ exports.default = { var type = this.type.split('put')[0]; return type.charAt(0).toUpperCase() + type.slice(1);; + }, + isImage: function isImage() { + if (this.outputType) { + return this.outputType.indexOf('image/') === 0; + } else { + return false; + } } } }; // @@ -316,6 +342,12 @@ exports.default = { // // // +// +// +// +// +// +// /***/ }), /* 5 */ @@ -1896,6 +1928,7 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c attrs: { "type": "output", "raw-code": _vm.outputText, + "output-type": _vm.outputType, "count": _vm.output.execution_count } }) : _vm._e()], 1) @@ -1982,10 +2015,14 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c "type": _vm.promptType, "count": _vm.count } - }), _vm._v(" "), _c('pre', { + }), _vm._v(" "), (!_vm.isImage) ? _c('pre', { domProps: { "innerHTML": _vm._s(_vm.code) } + }, [_vm._v("\n ")]) : _c('img', { + attrs: { + "src": 'data:' + _vm.outputType + ';base64,' + _vm.rawCode + } })], 1) },staticRenderFns: []} module.exports.render._withStripped = true -- cgit v1.2.1 From e8949a1ee48b5589c1f82d4b8a6b4e20d43d51a3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 22 Mar 2017 14:08:51 +0000 Subject: Updated notebooklab library Added error handling [ci skip] --- app/assets/javascripts/blob/notebook_viewer.js | 56 +- vendor/assets/javascripts/notebooklab.js | 1919 +++++++++++++++++++++--- 2 files changed, 1779 insertions(+), 196 deletions(-) diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js index 4bd80393d17..45b838c700f 100644 --- a/app/assets/javascripts/blob/notebook_viewer.js +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -5,11 +5,7 @@ import NotebookLab from 'vendor/notebooklab'; Vue.use(VueResource); Vue.use(NotebookLab); -Vue.config.errorHandler = (err) => { - console.log(err); -} - -$(() => { +document.addEventListener('DOMContentLoaded', () => { const el = document.getElementById('js-notebook-viewer'); new Vue({ @@ -17,21 +13,53 @@ $(() => { data() { return { error: false, + loadError: false, loading: true, json: {}, }; }, template: ` -
    - - +
    +
    + +
    +

    + + An error occured whilst loading the file. Please try again later. + + + An error occured whilst parsing the file. + +

    `, + methods: { + loadFile() { + this.$http.get(el.dataset.endpoint) + .then((res) => { + this.json = res.json(); + this.loading = false; + }) + .catch((e) => { + if (e.status) { + this.loadError = true; + } + + this.error = true; + }); + }, + }, mounted() { $('', { rel: 'stylesheet', @@ -40,11 +68,7 @@ $(() => { }).appendTo('head'); $.getScript(gon.katex_js_url, () => { - this.$http.get(el.dataset.endpoint) - .then((res) => { - this.json = res.json(); - this.loading = false; - }); + this.loadFile(); }); }, }); diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js index 06d41f03211..35e845657bd 100644 --- a/vendor/assets/javascripts/notebooklab.js +++ b/vendor/assets/javascripts/notebooklab.js @@ -73,7 +73,7 @@ return /******/ (function(modules) { // webpackBootstrap /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 19); +/******/ return __webpack_require__(__webpack_require__.s = 32); /******/ }) /************************************************************************/ /******/ ([ @@ -136,9 +136,9 @@ module.exports = function normalizeComponent ( var Component = __webpack_require__(0)( /* script */ - __webpack_require__(6), + __webpack_require__(10), /* template */ - __webpack_require__(15), + __webpack_require__(28), /* scopeId */ null, /* cssModules */ @@ -170,9 +170,43 @@ module.exports = Component.exports var Component = __webpack_require__(0)( /* script */ - __webpack_require__(7), + __webpack_require__(5), + /* template */ + __webpack_require__(30), + /* scopeId */ + null, + /* cssModules */ + null +) +Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code/index.vue" +if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")} +if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")} + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-d42105b8", Component.options) + } else { + hotAPI.reload("data-v-d42105b8", Component.options) + } +})()} + +module.exports = Component.exports + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +var Component = __webpack_require__(0)( + /* script */ + __webpack_require__(11), /* template */ - __webpack_require__(14), + __webpack_require__(27), /* scopeId */ null, /* cssModules */ @@ -199,7 +233,7 @@ module.exports = Component.exports /***/ }), -/* 3 */ +/* 4 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -209,15 +243,34 @@ Object.defineProperty(exports, "__esModule", { value: true }); -var _index = __webpack_require__(11); +var _index = __webpack_require__(2); var _index2 = _interopRequireDefault(_index); +var _index3 = __webpack_require__(22); + +var _index4 = _interopRequireDefault(_index3); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +// +// +// +// +// +// +// +// +// +// +// +// +// + exports.default = { components: { - 'code-cell': _index2.default + 'code-cell': _index2.default, + 'output-cell': _index4.default }, props: { cell: { @@ -234,46 +287,12 @@ exports.default = { }, output: function output() { return this.cell.outputs[0]; - }, - outputType: function outputType() { - if (!this.output.text) { - return Object.keys(this.output.data)[0]; - } - }, - outputText: function outputText() { - if (this.output.text) { - return this.output.text.join(''); - } else { - var output = this.output.data[this.outputType]; - - if (typeof output === 'array') { - return output.join(''); - } else if (typeof output === 'string') { - return output; - } else { - return ''; - } - } } } -}; // -// -// -// -// -// -// -// -// -// -// -// -// -// -// +}; /***/ }), -/* 4 */ +/* 5 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -283,12 +302,44 @@ Object.defineProperty(exports, "__esModule", { value: true }); +var _highlight = __webpack_require__(13); + +var _highlight2 = _interopRequireDefault(_highlight); + +var _xml = __webpack_require__(16); + +var _xml2 = _interopRequireDefault(_xml); + +var _javascript = __webpack_require__(14); + +var _javascript2 = _interopRequireDefault(_javascript); + +var _python = __webpack_require__(15); + +var _python2 = _interopRequireDefault(_python); + var _prompt = __webpack_require__(1); var _prompt2 = _interopRequireDefault(_prompt); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +_highlight2.default.registerLanguage('xml', _xml2.default); // +// +// +// +// +// +// +// +// +// +// +// + +_highlight2.default.registerLanguage('javascript', _javascript2.default); +_highlight2.default.registerLanguage('python', _python2.default); + exports.default = { components: { 'prompt': _prompt2.default @@ -298,11 +349,6 @@ exports.default = { type: Number, required: false }, - outputType: { - type: String, - required: false, - default: '' - }, type: { type: String, required: true @@ -314,43 +360,21 @@ exports.default = { }, computed: { code: function code() { - if (typeof hljs !== 'undefined' && this.promptType === 'In') { - return hljs.highlightAuto(this.rawCode).value; - } else { - return this.rawCode; - } + return this.rawCode; }, promptType: function promptType() { var type = this.type.split('put')[0]; return type.charAt(0).toUpperCase() + type.slice(1);; - }, - isImage: function isImage() { - if (this.outputType) { - return this.outputType.indexOf('image/') === 0; - } else { - return false; - } } + }, + mounted: function mounted() { + _highlight2.default.highlightBlock(this.$refs.code); } -}; // -// -// -// -// -// -// -// -// -// -// -// -// -// -// +}; /***/ }), -/* 5 */ +/* 6 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -360,7 +384,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -var _marked = __webpack_require__(9); +var _marked = __webpack_require__(17); var _marked2 = _interopRequireDefault(_marked); @@ -409,7 +433,7 @@ exports.default = { }; /***/ }), -/* 6 */ +/* 7 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -418,106 +442,1513 @@ exports.default = { Object.defineProperty(exports, "__esModule", { value: true }); + +var _prompt = __webpack_require__(1); + +var _prompt2 = _interopRequireDefault(_prompt); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = { + props: { + rawCode: { + type: String, + required: true + } + }, + components: { + 'prompt': _prompt2.default + } +}; // // // // // // // -// -// + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _prompt = __webpack_require__(1); + +var _prompt2 = _interopRequireDefault(_prompt); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } exports.default = { props: { - type: { + outputType: { type: String, - required: false + required: true }, + rawCode: { + type: String, + required: true + } + }, + components: { + 'prompt': _prompt2.default + } +}; // +// +// +// +// +// +// +// + +/***/ }), +/* 9 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; // +// +// +// +// +// +// +// + +var _index = __webpack_require__(2); + +var _index2 = _interopRequireDefault(_index); + +var _html = __webpack_require__(20); + +var _html2 = _interopRequireDefault(_html); + +var _image = __webpack_require__(21); + +var _image2 = _interopRequireDefault(_image); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = { + props: { count: { type: Number, - required: false + required: false, + default: false + }, + output: { + type: Object, + requred: true + } + }, + components: { + 'code-cell': _index2.default, + 'html-output': _html2.default, + 'image-output': _image2.default + }, + data: function data() { + return { + outputType: '' + }; + }, + + computed: { + componentName: function componentName() { + if (this.output.text) { + return 'code-cell'; + } else if (this.output.data['image/png']) { + this.outputType = 'image/png'; + + return 'image-output'; + } else if (this.output.data['text/html']) { + this.outputType = 'text/html'; + + return 'html-output'; + } else if (this.output.data['image/svg+xml']) { + this.outputType = 'image/svg+xml'; + + return 'html-output'; + } else { + this.outputType = 'text/plain'; + + return 'code-cell'; + } + }, + rawCode: function rawCode() { + if (this.output.text) { + return this.output.text.join(''); + } else { + return this.dataForType(this.outputType); + } + } + }, + methods: { + dataForType: function dataForType(type) { + var data = this.output.data[type]; + + if ((typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object') { + data = data.join(''); + } + + return data; } } }; /***/ }), -/* 7 */ +/* 10 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +// +// +// +// +// +// +// +// + +exports.default = { + props: { + type: { + type: String, + required: false + }, + count: { + type: Number, + required: false + } + } +}; + +/***/ }), +/* 11 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _cells = __webpack_require__(12); + +exports.default = { + components: { + 'code-cell': _cells.CodeCell, + 'markdown-cell': _cells.MarkdownCell + }, + props: { + notebook: { + type: Object, + required: true + } + }, + methods: { + cellType: function cellType(type) { + return type + '-cell'; + } + }, + computed: { + hasNotebook: function hasNotebook() { + return Object.keys(this.notebook).length; + } + } +}; // +// +// +// +// +// +// +// +// +// + +/***/ }), +/* 12 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _markdown = __webpack_require__(19); + +Object.defineProperty(exports, 'MarkdownCell', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_markdown).default; + } +}); + +var _code = __webpack_require__(18); + +Object.defineProperty(exports, 'CodeCell', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_code).default; + } +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/***/ }), +/* 13 */ +/***/ (function(module, exports, __webpack_require__) { + +/* +Syntax highlighting with language autodetection. +https://highlightjs.org/ +*/ + +(function(factory) { + + // Find the global object for export to both the browser and web workers. + var globalObject = typeof window === 'object' && window || + typeof self === 'object' && self; + + // Setup highlight.js for different environments. First is Node.js or + // CommonJS. + if(true) { + factory(exports); + } else if(globalObject) { + // Export hljs globally even when using AMD for cases when this script + // is loaded with others that may still expect a global hljs. + globalObject.hljs = factory({}); + + // Finally register the global hljs with AMD. + if(typeof define === 'function' && define.amd) { + define([], function() { + return globalObject.hljs; + }); + } + } + +}(function(hljs) { + // Convenience variables for build-in objects + var ArrayProto = [], + objectKeys = Object.keys; + + // Global internal variables used within the highlight.js library. + var languages = {}, + aliases = {}; + + // Regular expressions used throughout the highlight.js library. + var noHighlightRe = /^(no-?highlight|plain|text)$/i, + languagePrefixRe = /\blang(?:uage)?-([\w-]+)\b/i, + fixMarkupRe = /((^(<[^>]+>|\t|)+|(?:\n)))/gm; + + var spanEndTag = ''; + + // Global options used when within external APIs. This is modified when + // calling the `hljs.configure` function. + var options = { + classPrefix: 'hljs-', + tabReplace: null, + useBR: false, + languages: undefined + }; + + // Object map that is used to escape some common HTML characters. + var escapeRegexMap = { + '&': '&', + '<': '<', + '>': '>' + }; + + /* Utility functions */ + + function escape(value) { + return value.replace(/[&<>]/gm, function(character) { + return escapeRegexMap[character]; + }); + } + + function tag(node) { + return node.nodeName.toLowerCase(); + } + + function testRe(re, lexeme) { + var match = re && re.exec(lexeme); + return match && match.index === 0; + } + + function isNotHighlighted(language) { + return noHighlightRe.test(language); + } + + function blockLanguage(block) { + var i, match, length, _class; + var classes = block.className + ' '; + + classes += block.parentNode ? block.parentNode.className : ''; + + // language-* takes precedence over non-prefixed class names. + match = languagePrefixRe.exec(classes); + if (match) { + return getLanguage(match[1]) ? match[1] : 'no-highlight'; + } + + classes = classes.split(/\s+/); + + for (i = 0, length = classes.length; i < length; i++) { + _class = classes[i] + + if (isNotHighlighted(_class) || getLanguage(_class)) { + return _class; + } + } + } + + function inherit(parent) { // inherit(parent, override_obj, override_obj, ...) + var key; + var result = {}; + var objects = Array.prototype.slice.call(arguments, 1); + + for (key in parent) + result[key] = parent[key]; + objects.forEach(function(obj) { + for (key in obj) + result[key] = obj[key]; + }); + return result; + } + + /* Stream merging */ + + function nodeStream(node) { + var result = []; + (function _nodeStream(node, offset) { + for (var child = node.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 3) + offset += child.nodeValue.length; + else if (child.nodeType === 1) { + result.push({ + event: 'start', + offset: offset, + node: child + }); + offset = _nodeStream(child, offset); + // Prevent void elements from having an end tag that would actually + // double them in the output. There are more void elements in HTML + // but we list only those realistically expected in code display. + if (!tag(child).match(/br|hr|img|input/)) { + result.push({ + event: 'stop', + offset: offset, + node: child + }); + } + } + } + return offset; + })(node, 0); + return result; + } + + function mergeStreams(original, highlighted, value) { + var processed = 0; + var result = ''; + var nodeStack = []; + + function selectStream() { + if (!original.length || !highlighted.length) { + return original.length ? original : highlighted; + } + if (original[0].offset !== highlighted[0].offset) { + return (original[0].offset < highlighted[0].offset) ? original : highlighted; + } + + /* + To avoid starting the stream just before it should stop the order is + ensured that original always starts first and closes last: + + if (event1 == 'start' && event2 == 'start') + return original; + if (event1 == 'start' && event2 == 'stop') + return highlighted; + if (event1 == 'stop' && event2 == 'start') + return original; + if (event1 == 'stop' && event2 == 'stop') + return highlighted; + + ... which is collapsed to: + */ + return highlighted[0].event === 'start' ? original : highlighted; + } + + function open(node) { + function attr_str(a) {return ' ' + a.nodeName + '="' + escape(a.value) + '"';} + result += '<' + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join('') + '>'; + } + + function close(node) { + result += ''; + } + + function render(event) { + (event.event === 'start' ? open : close)(event.node); + } + + while (original.length || highlighted.length) { + var stream = selectStream(); + result += escape(value.substring(processed, stream[0].offset)); + processed = stream[0].offset; + if (stream === original) { + /* + On any opening or closing tag of the original markup we first close + the entire highlighted node stack, then render the original tag along + with all the following original tags at the same offset and then + reopen all the tags on the highlighted stack. + */ + nodeStack.reverse().forEach(close); + do { + render(stream.splice(0, 1)[0]); + stream = selectStream(); + } while (stream === original && stream.length && stream[0].offset === processed); + nodeStack.reverse().forEach(open); + } else { + if (stream[0].event === 'start') { + nodeStack.push(stream[0].node); + } else { + nodeStack.pop(); + } + render(stream.splice(0, 1)[0]); + } + } + return result + escape(value.substr(processed)); + } + + /* Initialization */ + + function expand_mode(mode) { + if (mode.variants && !mode.cached_variants) { + mode.cached_variants = mode.variants.map(function(variant) { + return inherit(mode, {variants: null}, variant); + }); + } + return mode.cached_variants || (mode.endsWithParent && [inherit(mode)]) || [mode]; + } + + function compileLanguage(language) { + + function reStr(re) { + return (re && re.source) || re; + } + + function langRe(value, global) { + return new RegExp( + reStr(value), + 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') + ); + } + + function compileMode(mode, parent) { + if (mode.compiled) + return; + mode.compiled = true; + + mode.keywords = mode.keywords || mode.beginKeywords; + if (mode.keywords) { + var compiled_keywords = {}; + + var flatten = function(className, str) { + if (language.case_insensitive) { + str = str.toLowerCase(); + } + str.split(' ').forEach(function(kw) { + var pair = kw.split('|'); + compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1]; + }); + }; + + if (typeof mode.keywords === 'string') { // string + flatten('keyword', mode.keywords); + } else { + objectKeys(mode.keywords).forEach(function (className) { + flatten(className, mode.keywords[className]); + }); + } + mode.keywords = compiled_keywords; + } + mode.lexemesRe = langRe(mode.lexemes || /\w+/, true); + + if (parent) { + if (mode.beginKeywords) { + mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\b'; + } + if (!mode.begin) + mode.begin = /\B|\b/; + mode.beginRe = langRe(mode.begin); + if (!mode.end && !mode.endsWithParent) + mode.end = /\B|\b/; + if (mode.end) + mode.endRe = langRe(mode.end); + mode.terminator_end = reStr(mode.end) || ''; + if (mode.endsWithParent && parent.terminator_end) + mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end; + } + if (mode.illegal) + mode.illegalRe = langRe(mode.illegal); + if (mode.relevance == null) + mode.relevance = 1; + if (!mode.contains) { + mode.contains = []; + } + mode.contains = Array.prototype.concat.apply([], mode.contains.map(function(c) { + return expand_mode(c === 'self' ? mode : c) + })); + mode.contains.forEach(function(c) {compileMode(c, mode);}); + + if (mode.starts) { + compileMode(mode.starts, parent); + } + + var terminators = + mode.contains.map(function(c) { + return c.beginKeywords ? '\\.?(' + c.begin + ')\\.?' : c.begin; + }) + .concat([mode.terminator_end, mode.illegal]) + .map(reStr) + .filter(Boolean); + mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(/*s*/) {return null;}}; + } + + compileMode(language); + } + + /* + Core highlighting function. Accepts a language name, or an alias, and a + string with the code to highlight. Returns an object with the following + properties: + + - relevance (int) + - value (an HTML string with highlighting markup) + + */ + function highlight(name, value, ignore_illegals, continuation) { + + function subMode(lexeme, mode) { + var i, length; + + for (i = 0, length = mode.contains.length; i < length; i++) { + if (testRe(mode.contains[i].beginRe, lexeme)) { + return mode.contains[i]; + } + } + } + + function endOfMode(mode, lexeme) { + if (testRe(mode.endRe, lexeme)) { + while (mode.endsParent && mode.parent) { + mode = mode.parent; + } + return mode; + } + if (mode.endsWithParent) { + return endOfMode(mode.parent, lexeme); + } + } + + function isIllegal(lexeme, mode) { + return !ignore_illegals && testRe(mode.illegalRe, lexeme); + } + + function keywordMatch(mode, match) { + var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0]; + return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str]; + } + + function buildSpan(classname, insideSpan, leaveOpen, noPrefix) { + var classPrefix = noPrefix ? '' : options.classPrefix, + openSpan = ''; + + return openSpan + insideSpan + closeSpan; + } + + function processKeywords() { + var keyword_match, last_index, match, result; + + if (!top.keywords) + return escape(mode_buffer); + + result = ''; + last_index = 0; + top.lexemesRe.lastIndex = 0; + match = top.lexemesRe.exec(mode_buffer); + + while (match) { + result += escape(mode_buffer.substring(last_index, match.index)); + keyword_match = keywordMatch(top, match); + if (keyword_match) { + relevance += keyword_match[1]; + result += buildSpan(keyword_match[0], escape(match[0])); + } else { + result += escape(match[0]); + } + last_index = top.lexemesRe.lastIndex; + match = top.lexemesRe.exec(mode_buffer); + } + return result + escape(mode_buffer.substr(last_index)); + } + + function processSubLanguage() { + var explicit = typeof top.subLanguage === 'string'; + if (explicit && !languages[top.subLanguage]) { + return escape(mode_buffer); + } + + var result = explicit ? + highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) : + highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined); + + // Counting embedded language score towards the host language may be disabled + // with zeroing the containing mode relevance. Usecase in point is Markdown that + // allows XML everywhere and makes every XML snippet to have a much larger Markdown + // score. + if (top.relevance > 0) { + relevance += result.relevance; + } + if (explicit) { + continuations[top.subLanguage] = result.top; + } + return buildSpan(result.language, result.value, false, true); + } + + function processBuffer() { + result += (top.subLanguage != null ? processSubLanguage() : processKeywords()); + mode_buffer = ''; + } + + function startNewMode(mode) { + result += mode.className? buildSpan(mode.className, '', true): ''; + top = Object.create(mode, {parent: {value: top}}); + } + + function processLexeme(buffer, lexeme) { + + mode_buffer += buffer; + + if (lexeme == null) { + processBuffer(); + return 0; + } + + var new_mode = subMode(lexeme, top); + if (new_mode) { + if (new_mode.skip) { + mode_buffer += lexeme; + } else { + if (new_mode.excludeBegin) { + mode_buffer += lexeme; + } + processBuffer(); + if (!new_mode.returnBegin && !new_mode.excludeBegin) { + mode_buffer = lexeme; + } + } + startNewMode(new_mode, lexeme); + return new_mode.returnBegin ? 0 : lexeme.length; + } + + var end_mode = endOfMode(top, lexeme); + if (end_mode) { + var origin = top; + if (origin.skip) { + mode_buffer += lexeme; + } else { + if (!(origin.returnEnd || origin.excludeEnd)) { + mode_buffer += lexeme; + } + processBuffer(); + if (origin.excludeEnd) { + mode_buffer = lexeme; + } + } + do { + if (top.className) { + result += spanEndTag; + } + if (!top.skip) { + relevance += top.relevance; + } + top = top.parent; + } while (top !== end_mode.parent); + if (end_mode.starts) { + startNewMode(end_mode.starts, ''); + } + return origin.returnEnd ? 0 : lexeme.length; + } + + if (isIllegal(lexeme, top)) + throw new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"'); + + /* + Parser should not reach this point as all types of lexemes should be caught + earlier, but if it does due to some bug make sure it advances at least one + character forward to prevent infinite looping. + */ + mode_buffer += lexeme; + return lexeme.length || 1; + } + + var language = getLanguage(name); + if (!language) { + throw new Error('Unknown language: "' + name + '"'); + } + + compileLanguage(language); + var top = continuation || language; + var continuations = {}; // keep continuations for sub-languages + var result = '', current; + for(current = top; current !== language; current = current.parent) { + if (current.className) { + result = buildSpan(current.className, '', true) + result; + } + } + var mode_buffer = ''; + var relevance = 0; + try { + var match, count, index = 0; + while (true) { + top.terminators.lastIndex = index; + match = top.terminators.exec(value); + if (!match) + break; + count = processLexeme(value.substring(index, match.index), match[0]); + index = match.index + count; + } + processLexeme(value.substr(index)); + for(current = top; current.parent; current = current.parent) { // close dangling modes + if (current.className) { + result += spanEndTag; + } + } + return { + relevance: relevance, + value: result, + language: name, + top: top + }; + } catch (e) { + if (e.message && e.message.indexOf('Illegal') !== -1) { + return { + relevance: 0, + value: escape(value) + }; + } else { + throw e; + } + } + } + + /* + Highlighting with language detection. Accepts a string with the code to + highlight. Returns an object with the following properties: + + - language (detected language) + - relevance (int) + - value (an HTML string with highlighting markup) + - second_best (object with the same structure for second-best heuristically + detected language, may be absent) + + */ + function highlightAuto(text, languageSubset) { + languageSubset = languageSubset || options.languages || objectKeys(languages); + var result = { + relevance: 0, + value: escape(text) + }; + var second_best = result; + languageSubset.filter(getLanguage).forEach(function(name) { + var current = highlight(name, text, false); + current.language = name; + if (current.relevance > second_best.relevance) { + second_best = current; + } + if (current.relevance > result.relevance) { + second_best = result; + result = current; + } + }); + if (second_best.language) { + result.second_best = second_best; + } + return result; + } + + /* + Post-processing of the highlighted markup: + + - replace TABs with something more useful + - replace real line-breaks with '
    ' for non-pre containers + + */ + function fixMarkup(value) { + return !(options.tabReplace || options.useBR) + ? value + : value.replace(fixMarkupRe, function(match, p1) { + if (options.useBR && match === '\n') { + return '
    '; + } else if (options.tabReplace) { + return p1.replace(/\t/g, options.tabReplace); + } + return ''; + }); + } + + function buildClassName(prevClassName, currentLang, resultLang) { + var language = currentLang ? aliases[currentLang] : resultLang, + result = [prevClassName.trim()]; + + if (!prevClassName.match(/\bhljs\b/)) { + result.push('hljs'); + } + + if (prevClassName.indexOf(language) === -1) { + result.push(language); + } + + return result.join(' ').trim(); + } + + /* + Applies highlighting to a DOM node containing code. Accepts a DOM node and + two optional parameters for fixMarkup. + */ + function highlightBlock(block) { + var node, originalStream, result, resultNode, text; + var language = blockLanguage(block); + + if (isNotHighlighted(language)) + return; + + if (options.useBR) { + node = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); + node.innerHTML = block.innerHTML.replace(/\n/g, '').replace(//g, '\n'); + } else { + node = block; + } + text = node.textContent; + result = language ? highlight(language, text, true) : highlightAuto(text); + + originalStream = nodeStream(node); + if (originalStream.length) { + resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); + resultNode.innerHTML = result.value; + result.value = mergeStreams(originalStream, nodeStream(resultNode), text); + } + result.value = fixMarkup(result.value); + + block.innerHTML = result.value; + block.className = buildClassName(block.className, language, result.language); + block.result = { + language: result.language, + re: result.relevance + }; + if (result.second_best) { + block.second_best = { + language: result.second_best.language, + re: result.second_best.relevance + }; + } + } + + /* + Updates highlight.js global options with values passed in the form of an object. + */ + function configure(user_options) { + options = inherit(options, user_options); + } + /* + Applies highlighting to all
    ..
    blocks on a page. + */ + function initHighlighting() { + if (initHighlighting.called) + return; + initHighlighting.called = true; -Object.defineProperty(exports, "__esModule", { - value: true -}); + var blocks = document.querySelectorAll('pre code'); + ArrayProto.forEach.call(blocks, highlightBlock); + } -var _cells = __webpack_require__(8); + /* + Attaches highlighting to the page load event. + */ + function initHighlightingOnLoad() { + addEventListener('DOMContentLoaded', initHighlighting, false); + addEventListener('load', initHighlighting, false); + } -exports.default = { - components: { - 'code-cell': _cells.CodeCell, - 'markdown-cell': _cells.MarkdownCell - }, - props: { - notebook: { - type: Object, - required: true - } - }, - methods: { - cellType: function cellType(type) { - return type + '-cell'; - } - }, - computed: { - hasNotebook: function hasNotebook() { - return Object.keys(this.notebook).length; + function registerLanguage(name, language) { + var lang = languages[name] = language(hljs); + if (lang.aliases) { + lang.aliases.forEach(function(alias) {aliases[alias] = name;}); } } -}; // -// -// -// -// -// -// -// -// -// -/***/ }), -/* 8 */ -/***/ (function(module, exports, __webpack_require__) { + function listLanguages() { + return objectKeys(languages); + } -"use strict"; + function getLanguage(name) { + name = (name || '').toLowerCase(); + return languages[name] || languages[aliases[name]]; + } + /* Interface definition */ + + hljs.highlight = highlight; + hljs.highlightAuto = highlightAuto; + hljs.fixMarkup = fixMarkup; + hljs.highlightBlock = highlightBlock; + hljs.configure = configure; + hljs.initHighlighting = initHighlighting; + hljs.initHighlightingOnLoad = initHighlightingOnLoad; + hljs.registerLanguage = registerLanguage; + hljs.listLanguages = listLanguages; + hljs.getLanguage = getLanguage; + hljs.inherit = inherit; + + // Common regexps + hljs.IDENT_RE = '[a-zA-Z]\\w*'; + hljs.UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; + hljs.NUMBER_RE = '\\b\\d+(\\.\\d+)?'; + hljs.C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float + hljs.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... + hljs.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; + + // Common modes + hljs.BACKSLASH_ESCAPE = { + begin: '\\\\[\\s\\S]', relevance: 0 + }; + hljs.APOS_STRING_MODE = { + className: 'string', + begin: '\'', end: '\'', + illegal: '\\n', + contains: [hljs.BACKSLASH_ESCAPE] + }; + hljs.QUOTE_STRING_MODE = { + className: 'string', + begin: '"', end: '"', + illegal: '\\n', + contains: [hljs.BACKSLASH_ESCAPE] + }; + hljs.PHRASAL_WORDS_MODE = { + begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/ + }; + hljs.COMMENT = function (begin, end, inherits) { + var mode = hljs.inherit( + { + className: 'comment', + begin: begin, end: end, + contains: [] + }, + inherits || {} + ); + mode.contains.push(hljs.PHRASAL_WORDS_MODE); + mode.contains.push({ + className: 'doctag', + begin: '(?:TODO|FIXME|NOTE|BUG|XXX):', + relevance: 0 + }); + return mode; + }; + hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$'); + hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\*', '\\*/'); + hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$'); + hljs.NUMBER_MODE = { + className: 'number', + begin: hljs.NUMBER_RE, + relevance: 0 + }; + hljs.C_NUMBER_MODE = { + className: 'number', + begin: hljs.C_NUMBER_RE, + relevance: 0 + }; + hljs.BINARY_NUMBER_MODE = { + className: 'number', + begin: hljs.BINARY_NUMBER_RE, + relevance: 0 + }; + hljs.CSS_NUMBER_MODE = { + className: 'number', + begin: hljs.NUMBER_RE + '(' + + '%|em|ex|ch|rem' + + '|vw|vh|vmin|vmax' + + '|cm|mm|in|pt|pc|px' + + '|deg|grad|rad|turn' + + '|s|ms' + + '|Hz|kHz' + + '|dpi|dpcm|dppx' + + ')?', + relevance: 0 + }; + hljs.REGEXP_MODE = { + className: 'regexp', + begin: /\//, end: /\/[gimuy]*/, + illegal: /\n/, + contains: [ + hljs.BACKSLASH_ESCAPE, + { + begin: /\[/, end: /\]/, + relevance: 0, + contains: [hljs.BACKSLASH_ESCAPE] + } + ] + }; + hljs.TITLE_MODE = { + className: 'title', + begin: hljs.IDENT_RE, + relevance: 0 + }; + hljs.UNDERSCORE_TITLE_MODE = { + className: 'title', + begin: hljs.UNDERSCORE_IDENT_RE, + relevance: 0 + }; + hljs.METHOD_GUARD = { + // excludes method names from keyword processing + begin: '\\.\\s*' + hljs.UNDERSCORE_IDENT_RE, + relevance: 0 + }; -Object.defineProperty(exports, "__esModule", { - value: true -}); + return hljs; +})); -var _markdown = __webpack_require__(12); -Object.defineProperty(exports, 'MarkdownCell', { - enumerable: true, - get: function get() { - return _interopRequireDefault(_markdown).default; - } -}); +/***/ }), +/* 14 */ +/***/ (function(module, exports) { -var _code = __webpack_require__(10); +module.exports = function(hljs) { + var IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; + var KEYWORDS = { + keyword: + 'in of if for while finally var new function do return void else break catch ' + + 'instanceof with throw case default try this switch continue typeof delete ' + + 'let yield const export super debugger as async await static ' + + // ECMAScript 6 modules import + 'import from as' + , + literal: + 'true false null undefined NaN Infinity', + built_in: + 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + + 'module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect ' + + 'Promise' + }; + var EXPRESSIONS; + var NUMBER = { + className: 'number', + variants: [ + { begin: '\\b(0[bB][01]+)' }, + { begin: '\\b(0[oO][0-7]+)' }, + { begin: hljs.C_NUMBER_RE } + ], + relevance: 0 + }; + var SUBST = { + className: 'subst', + begin: '\\$\\{', end: '\\}', + keywords: KEYWORDS, + contains: [] // defined later + }; + var TEMPLATE_STRING = { + className: 'string', + begin: '`', end: '`', + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ] + }; + SUBST.contains = [ + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + TEMPLATE_STRING, + NUMBER, + hljs.REGEXP_MODE + ] + var PARAMS_CONTAINS = SUBST.contains.concat([ + hljs.C_BLOCK_COMMENT_MODE, + hljs.C_LINE_COMMENT_MODE + ]); -Object.defineProperty(exports, 'CodeCell', { - enumerable: true, - get: function get() { - return _interopRequireDefault(_code).default; - } -}); + return { + aliases: ['js', 'jsx'], + keywords: KEYWORDS, + contains: [ + { + className: 'meta', + relevance: 10, + begin: /^\s*['"]use (strict|asm)['"]/ + }, + { + className: 'meta', + begin: /^#!/, end: /$/ + }, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + TEMPLATE_STRING, + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE, + NUMBER, + { // object attr container + begin: /[{,]\s*/, relevance: 0, + contains: [ + { + begin: IDENT_RE + '\\s*:', returnBegin: true, + relevance: 0, + contains: [{className: 'attr', begin: IDENT_RE, relevance: 0}] + } + ] + }, + { // "value" container + begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', + keywords: 'return throw case', + contains: [ + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE, + hljs.REGEXP_MODE, + { + className: 'function', + begin: '(\\(.*?\\)|' + IDENT_RE + ')\\s*=>', returnBegin: true, + end: '\\s*=>', + contains: [ + { + className: 'params', + variants: [ + { + begin: IDENT_RE + }, + { + begin: /\(\s*\)/, + }, + { + begin: /\(/, end: /\)/, + excludeBegin: true, excludeEnd: true, + keywords: KEYWORDS, + contains: PARAMS_CONTAINS + } + ] + } + ] + }, + { // E4X / JSX + begin: //, + subLanguage: 'xml', + contains: [ + {begin: /<\w+\s*\/>/, skip: true}, + { + begin: /<\w+/, end: /(\/\w+|\w+\/)>/, skip: true, + contains: [ + {begin: /<\w+\s*\/>/, skip: true}, + 'self' + ] + } + ] + } + ], + relevance: 0 + }, + { + className: 'function', + beginKeywords: 'function', end: /\{/, excludeEnd: true, + contains: [ + hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), + { + className: 'params', + begin: /\(/, end: /\)/, + excludeBegin: true, + excludeEnd: true, + contains: PARAMS_CONTAINS + } + ], + illegal: /\[|%/ + }, + { + begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` + }, + hljs.METHOD_GUARD, + { // ES6 class + className: 'class', + beginKeywords: 'class', end: /[{;=]/, excludeEnd: true, + illegal: /[:"\[\]]/, + contains: [ + {beginKeywords: 'extends'}, + hljs.UNDERSCORE_TITLE_MODE + ] + }, + { + beginKeywords: 'constructor', end: /\{/, excludeEnd: true + } + ], + illegal: /#(?!!)/ + }; +}; -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +/***/ }), +/* 15 */ +/***/ (function(module, exports) { + +module.exports = function(hljs) { + var KEYWORDS = { + keyword: + 'and elif is global as in if from raise for except finally print import pass return ' + + 'exec else break not with class assert yield try while continue del or def lambda ' + + 'async await nonlocal|10 None True False', + built_in: + 'Ellipsis NotImplemented' + }; + var PROMPT = { + className: 'meta', begin: /^(>>>|\.\.\.) / + }; + var SUBST = { + className: 'subst', + begin: /\{/, end: /\}/, + keywords: KEYWORDS, + illegal: /#/ + }; + var STRING = { + className: 'string', + contains: [hljs.BACKSLASH_ESCAPE], + variants: [ + { + begin: /(u|b)?r?'''/, end: /'''/, + contains: [PROMPT], + relevance: 10 + }, + { + begin: /(u|b)?r?"""/, end: /"""/, + contains: [PROMPT], + relevance: 10 + }, + { + begin: /(fr|rf|f)'''/, end: /'''/, + contains: [PROMPT, SUBST] + }, + { + begin: /(fr|rf|f)"""/, end: /"""/, + contains: [PROMPT, SUBST] + }, + { + begin: /(u|r|ur)'/, end: /'/, + relevance: 10 + }, + { + begin: /(u|r|ur)"/, end: /"/, + relevance: 10 + }, + { + begin: /(b|br)'/, end: /'/ + }, + { + begin: /(b|br)"/, end: /"/ + }, + { + begin: /(fr|rf|f)'/, end: /'/, + contains: [SUBST] + }, + { + begin: /(fr|rf|f)"/, end: /"/, + contains: [SUBST] + }, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE + ] + }; + var NUMBER = { + className: 'number', relevance: 0, + variants: [ + {begin: hljs.BINARY_NUMBER_RE + '[lLjJ]?'}, + {begin: '\\b(0o[0-7]+)[lLjJ]?'}, + {begin: hljs.C_NUMBER_RE + '[lLjJ]?'} + ] + }; + var PARAMS = { + className: 'params', + begin: /\(/, end: /\)/, + contains: ['self', PROMPT, NUMBER, STRING] + }; + SUBST.contains = [STRING, NUMBER, PROMPT]; + return { + aliases: ['py', 'gyp'], + keywords: KEYWORDS, + illegal: /(<\/|->|\?)|=>/, + contains: [ + PROMPT, + NUMBER, + STRING, + hljs.HASH_COMMENT_MODE, + { + variants: [ + {className: 'function', beginKeywords: 'def'}, + {className: 'class', beginKeywords: 'class'} + ], + end: /:/, + illegal: /[${=;\n,]/, + contains: [ + hljs.UNDERSCORE_TITLE_MODE, + PARAMS, + { + begin: /->/, endsWithParent: true, + keywords: 'None' + } + ] + }, + { + className: 'meta', + begin: /^[\t ]*@/, end: /$/ + }, + { + begin: /\b(print|exec)\(/ // don’t highlight keywords-turned-functions in Python 3 + } + ] + }; +}; /***/ }), -/* 9 */ +/* 16 */ +/***/ (function(module, exports) { + +module.exports = function(hljs) { + var XML_IDENT_RE = '[A-Za-z0-9\\._:-]+'; + var TAG_INTERNALS = { + endsWithParent: true, + illegal: /`]+/} + ] + } + ] + } + ] + }; + return { + aliases: ['html', 'xhtml', 'rss', 'atom', 'xjb', 'xsd', 'xsl', 'plist'], + case_insensitive: true, + contains: [ + { + className: 'meta', + begin: '', + relevance: 10, + contains: [{begin: '\\[', end: '\\]'}] + }, + hljs.COMMENT( + '', + { + relevance: 10 + } + ), + { + begin: '<\\!\\[CDATA\\[', end: '\\]\\]>', + relevance: 10 + }, + { + begin: /<\?(php)?/, end: /\?>/, + subLanguage: 'php', + contains: [{begin: '/\\*', end: '\\*/', skip: true}] + }, + { + className: 'tag', + /* + The lookahead pattern (?=...) ensures that 'begin' only matches + '|$)', end: '>', + keywords: {name: 'style'}, + contains: [TAG_INTERNALS], + starts: { + end: '', returnEnd: true, + subLanguage: ['css', 'xml'] + } + }, + { + className: 'tag', + // See the comment in the ', returnEnd: true, - subLanguage: ['css', 'xml'] - } - }, - { - className: 'tag', - // See the comment in the + +#### Understanding Guides, Tutorials, and Technical Overviews Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. @@ -39,6 +41,11 @@ A **tutorial** requires a clear **step-by-step** guidance to achieve a singular - Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) +A **technical overview** is an overview of that feature. It describes that it is, and what it does, but does not walks +through the process of how to use it systematically. + +- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) + #### Special Format Every **Technical Article** contains, in the very beginning, a blockquote with the following information: -- cgit v1.2.1 From 6fdd595e591f90cb297a7e59706139857da130d8 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 27 Mar 2017 20:21:46 -0300 Subject: improve wording, update links --- doc/development/writing_documentation.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 1f65b863a36..ca0d8a3f775 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -1,7 +1,7 @@ # Writing Documentation - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - - **Technical Articles**: written by any GitLab Team member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). + - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code. ## Distinction between General Documentation and Technical Articles @@ -33,15 +33,16 @@ They live under `doc/topics/topic-name/`, and can be searched per topic, within Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. -A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, the step 3, but, assumes that that step is known by the reader, or out of the scope of that article. +A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them. - Live example: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.html) to [Part 4](../user/project/pages/getting_started_part_one.html)" -A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. It does not assume that you step 3: it shows you each of them. +A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. +It does not only describes steps 2 and 3, but also shows you how to accomplish them. - Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) -A **technical overview** is an overview of that feature. It describes that it is, and what it does, but does not walks +A **technical overview** is a description of what a certain feature is, and what it does, but does not walk through the process of how to use it systematically. - Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) @@ -62,7 +63,7 @@ Every **Technical Article** contains, in the very beginning, a blockquote with t #### Technical Articles - Writing Method -Use the [writing method](https://about.gitlab.com/handbook/marketing/developer-relations/technical-writing/#writing-method) defined by the Technical Writing team. +Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team. ## Documentation Style Guidelines @@ -70,4 +71,4 @@ All the documentation follow the same [styleguide](https://docs.gitlab.com/ce/de ### Markdown -Currently GitLab docs use Redcarpet as markdown engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. +Currently GitLab docs use Redcarpet as [markdown](../user/markdown.html) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. -- cgit v1.2.1 From d7b002f1a7c5cb8791e806804d4ace80847f9c5e Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 27 Mar 2017 22:22:11 -0300 Subject: add existing links to topics/index.md --- doc/topics/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/topics/index.md b/doc/topics/index.md index 43a4ad7422b..aaba4aefbfc 100644 --- a/doc/topics/index.md +++ b/doc/topics/index.md @@ -10,7 +10,7 @@ related to an specific subject or theme, including: ## Topics - Idea to Production - - GitLab Installation + - [GitLab Installation](../install/README.md) - GitLab Workflow - Chat - Issue @@ -23,9 +23,9 @@ related to an specific subject or theme, including: - Production - Feedback - Authentication -- Continuous Integration (GitLab CI) +- [Continuous Integration (GitLab CI)](../ci/) - GitLab Flow (branching strategy) -- [GitLab Pages](pages/) +- [GitLab Pages](../user/project/pages/) - Integrations > Note: non-linked indexes are currently under development and subjected to change. -- cgit v1.2.1 From cde35cb24602a4425f99497e687eabb12fce57f3 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 27 Mar 2017 22:24:18 -0300 Subject: Add license to CE/doc --- doc/LICENSE | 404 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 doc/LICENSE diff --git a/doc/LICENSE b/doc/LICENSE new file mode 100644 index 00000000000..2172c34e256 --- /dev/null +++ b/doc/LICENSE @@ -0,0 +1,404 @@ +Copyright (c) 2016 GitLab B.V. + +The content of GitLab Documentation is licensed under +CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). + +Legal code (https://creativecommons.org/licenses/by-sa/4.0/legalcode), in plain text (https://creativecommons.org/licenses/by/4.0/legalcode.txt): + +---- + +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are +intended for use by those authorized to give the public +permission to use material in ways otherwise restricted by +copyright and certain other rights. Our licenses are +irrevocable. Licensors should read and understand the terms +and conditions of the license they choose before applying it. +Licensors should also secure all rights necessary before +applying our licenses so that the public can reuse the +material as expected. Licensors should clearly mark any +material not subject to the license. This includes other CC- +licensed material, or material used under an exception or +limitation to copyright. More considerations for licensors: +wiki.creativecommons.org/Considerations_for_licensors + +Considerations for the public: By using one of our public +licenses, a licensor grants the public permission to use the +licensed material under specified terms and conditions. If +the licensor's permission is not necessary for any reason--for +example, because of any applicable exception or limitation to +copyright--then that use is not regulated by the license. Our +licenses grant only permissions under copyright and certain +other rights that a licensor has authority to grant. Use of +the licensed material may still be restricted for other +reasons, including because others have copyright or other +rights in the material. A licensor may make special requests, +such as asking that all changes be marked or described. +Although not required by our licenses, you are encouraged to +respect those requests where reasonable. More_considerations +for the public: +wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + +a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + +d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + +g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + +i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + +a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + +a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + +b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + +c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + +a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + +b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + +c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + +a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + +a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + +c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + +d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. -- cgit v1.2.1 From 550adacb3e5cdb161b41cf72309a5d65498f5906 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 27 Mar 2017 21:27:25 -0500 Subject: Fix custom protected branch pattern jumping scroll position to top Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/30149 Use more semantic `