From f9eb3fc0aa3c99fd36d3f344dc95f6b4edad5e07 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 7 Mar 2016 18:26:15 +0100 Subject: WIP - initial export service and spec --- app/services/projects/import_export.rb | 23 ++++++ .../projects/import_export/export_service.rb | 15 ++++ .../projects/import_export/project_tree_saver.rb | 42 +++++++++++ .../import_export/project_tree_saver_spec.rb | 83 ++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 app/services/projects/import_export.rb create mode 100644 app/services/projects/import_export/export_service.rb create mode 100644 app/services/projects/import_export/project_tree_saver.rb create mode 100644 spec/services/projects/import_export/project_tree_saver_spec.rb diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb new file mode 100644 index 00000000000..f87b10390a5 --- /dev/null +++ b/app/services/projects/import_export.rb @@ -0,0 +1,23 @@ +module Projects + module ImportExport + extend self + + def export_path(project_name:) + File.join(storage_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export_#{project_name}") + end + + def project_atts + %i(id name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) + end + + def project_tree + %i(issues merge_requests labels milestones snippets releases events commit_statuses) + end + + private + + def storage_path + File.join(Settings.shared['path'], 'tmp/project_exports') + end + end +end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb new file mode 100644 index 00000000000..c89dd3d12ed --- /dev/null +++ b/app/services/projects/import_export/export_service.rb @@ -0,0 +1,15 @@ +module Projects + module ImportExport + class ExportService < BaseService + def execute(options = {}) + save_project_tree + end + + private + + def save_project_tree + Projects::ImportExport::ProjectTreeSaver.save(project: project) + end + end + end +end diff --git a/app/services/projects/import_export/project_tree_saver.rb b/app/services/projects/import_export/project_tree_saver.rb new file mode 100644 index 00000000000..4801c791ffe --- /dev/null +++ b/app/services/projects/import_export/project_tree_saver.rb @@ -0,0 +1,42 @@ +module Projects + module ImportExport + class ProjectTreeSaver + attr_reader :full_path + + def initialize(project: ) + @project = project + end + + def save + @full_path = File.join(export_path, project_filename) + save_to_disk + end + + private + + def save_to_disk + FileUtils.mkdir_p(export_path) + File.write(full_path, project_json_tree) + true + rescue + #TODO: handle error + false + end + + def export_path + @export_path ||= ImportExport.export_path(@project.name) + end + + def project_filename + # TODO sanitize name + "#{@project.name}.json" + end + + def project_json_tree + # TODO confirm children, also add subchildren (i.e comments) + # TODO confirm atts for children + @project.to_json(only: ImportExport.project_atts, include: ImportExport.project_tree) + end + end + end +end diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb new file mode 100644 index 00000000000..00e44a9f335 --- /dev/null +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Projects::ImportExport::ProjectTreeSaver, services: true do + describe :save do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project) } + + before(:each) do + project.team << [user, :master] + allow_any_instance_of(Projects::ImportExport::ProjectTreeSaver).to receive(:export_path).and_return(export_path) + end + + after(:each) do + FileUtils.rm_rf(export_path) + end + + it 'saves project successfully' do + expect(project_tree_saver.save).to be true + end + + it 'saves the correct json' do + project_tree_saver.save + expect(project_json(project_tree_saver.full_path)).to include({ "name" => project.name }) + end + end + + def project_json(filename) + JSON.parse(IO.read(filename)) + end + + # TODO: Remove this. Current JSON pretty printed: + # { + # "id": 1, + # "name": "searchable_project", + # "path": "gitlabhq", + # "description": null, + # "issues_enabled": true, + # "wall_enabled": false, + # "merge_requests_enabled": true, + # "wiki_enabled": true, + # "snippets_enabled": true, + # "visibility_level": 20, + # "archived": false, + # "issues": [ + # + # ], + # "merge_requests": [ + # + # ], + # "labels": [ + # + # ], + # "milestones": [ + # + # ], + # "snippets": [ + # + # ], + # "releases": [ + # + # ], + # "events": [ + # { + # "id": 1, + # "target_type": null, + # "target_id": null, + # "title": null, + # "data": null, + # "project_id": 1, + # "created_at": "2016-03-07T17:05:20.926Z", + # "updated_at": "2016-03-07T17:05:20.926Z", + # "action": 8, + # "author_id": 3 + # } + # ], + # "commit_statuses": [ + # + # ] + # } +end -- cgit v1.2.1 From e228453f47a98c1da51878fb9feeff8ae9ddd7a9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 7 Mar 2016 19:14:06 +0100 Subject: updated spec --- .../import_export/project_tree_saver_spec.rb | 24 ++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 00e44a9f335..b9768441bae 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -4,7 +4,8 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do describe :save do let(:user) { create(:user) } - let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:issue) { create(:issue, assignee: user) } + let!(:project) { create(:project, :public, name: 'searchable_project', issues: [issue] )} let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project) } @@ -21,9 +22,24 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do expect(project_tree_saver.save).to be true end - it 'saves the correct json' do - project_tree_saver.save - expect(project_json(project_tree_saver.full_path)).to include({ "name" => project.name }) + context 'JSON' do + + let(:saved_project_json) do + project_tree_saver.save + project_json(project_tree_saver.full_path) + end + + it 'saves the correct json' do + expect(saved_project_json).to include({ "name" => project.name }) + end + + it 'has events' do + expect(saved_project_json['events']).not_to be_empty + end + + it 'has issues' do + expect(saved_project_json['issues']).not_to be_empty + end end end -- cgit v1.2.1 From 68eae04538f296a15863235d7f1f59f3e9e8a1dd Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 8 Mar 2016 10:21:02 +0100 Subject: finish project tree saver spec --- .../import_export/project_tree_saver_spec.rb | 221 +++++++++++++++++---- 1 file changed, 185 insertions(+), 36 deletions(-) diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index b9768441bae..42302ffee8f 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -5,7 +5,23 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do let(:user) { create(:user) } let(:issue) { create(:issue, assignee: user) } - let!(:project) { create(:project, :public, name: 'searchable_project', issues: [issue] )} + let(:merge_request) { create(:merge_request) } + let(:label) { create(:label) } + let(:snippet) { create(:project_snippet) } + let(:commit_status) { create(:commit_status) } + let(:release) { create(:release) } + let!(:project) do + create(:project, + :public, + name: 'searchable_project', + issues: [issue], + merge_requests: [merge_request], + labels: [label], + snippets: [snippet], + releases: [release], + commit_statuses: [commit_status]) + end + let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project) } @@ -37,9 +53,33 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['events']).not_to be_empty end + it 'has milestones' do + expect(saved_project_json['milestones']).not_to be_empty + end + + it 'has merge requests' do + expect(saved_project_json['merge_requests']).not_to be_empty + end + + it 'has labels' do + expect(saved_project_json['labels']).not_to be_empty + end + + it 'has snippets' do + expect(saved_project_json['snippets']).not_to be_empty + end + + it 'has releases' do + expect(saved_project_json['releases']).not_to be_empty + end + it 'has issues' do expect(saved_project_json['issues']).not_to be_empty end + + it 'has commit statuses' do + expect(saved_project_json['commit_statuses']).not_to be_empty + end end end @@ -49,51 +89,160 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do # TODO: Remove this. Current JSON pretty printed: # { - # "id": 1, - # "name": "searchable_project", - # "path": "gitlabhq", - # "description": null, - # "issues_enabled": true, - # "wall_enabled": false, - # "merge_requests_enabled": true, - # "wiki_enabled": true, - # "snippets_enabled": true, - # "visibility_level": 20, - # "archived": false, - # "issues": [ - # + # "id":7, + # "name":"searchable_project", + # "path":"gitlabhq", + # "description":null, + # "issues_enabled":true, + # "wall_enabled":false, + # "merge_requests_enabled":true, + # "wiki_enabled":true, + # "snippets_enabled":true, + # "visibility_level":20, + # "archived":false, + # "issues":[ + # { + # "id":1, + # "title":"Voluptas dolores molestias iste excepturi quia atque sint et.", + # "assignee_id":1, + # "author_id":2, + # "project_id":7, + # "created_at":"2016-03-08T09:14:31.726Z", + # "updated_at":"2016-03-08T09:14:36.293Z", + # "position":0, + # "branch_name":null, + # "description":null, + # "milestone_id":null, + # "state":"opened", + # "iid":1, + # "updated_by_id":null + # } # ], - # "merge_requests": [ + # "merge_requests":[ + # { + # "id":1, + # "target_branch":"feature", + # "source_branch":"master", + # "source_project_id":2, + # "author_id":5, + # "assignee_id":null, + # "title":"Quam velit cupiditate culpa perspiciatis esse maiores quaerat.", + # "created_at":"2016-03-08T09:14:32.597Z", + # "updated_at":"2016-03-08T09:14:32.597Z", + # "milestone_id":null, + # "state":"opened", + # "merge_status":"can_be_merged", + # "target_project_id":7, + # "iid":1, + # "description":null, + # "position":0, + # "locked_at":null, + # "updated_by_id":null, + # "merge_error":null, + # "merge_params":{ # + # }, + # "merge_when_build_succeeds":false, + # "merge_user_id":null, + # "merge_commit_sha":null + # } # ], - # "labels": [ - # + # "labels":[ + # { + # "id":1, + # "title":"Bug", + # "color":"#990000", + # "project_id":7, + # "created_at":"2016-03-08T09:14:33.774Z", + # "updated_at":"2016-03-08T09:14:36.314Z", + # "template":false, + # "description":null + # } # ], - # "milestones": [ - # + # "milestones":[ + # { + # "id":1, + # "title":"Milestone v1.2", + # "project_id":7, + # "description":null, + # "due_date":null, + # "created_at":"2016-03-08T09:14:36.526Z", + # "updated_at":"2016-03-08T09:14:36.526Z", + # "state":"active", + # "iid":1 + # } # ], - # "snippets": [ - # + # "snippets":[ + # { + # "id":1, + # "title":"Voluptatem qui officiis modi ut fugit distinctio dolor qui.", + # "content":"Quaerat sunt eligendi voluptatum magnam.", + # "author_id":12, + # "project_id":7, + # "created_at":"2016-03-08T09:14:34.539Z", + # "updated_at":"2016-03-08T09:14:36.332Z", + # "file_name":"rowland.tremblay", + # "expires_at":null, + # "visibility_level":0 + # } # ], - # "releases": [ - # + # "releases":[ + # { + # "id":1, + # "tag":"v1.1.0", + # "description":"Awesome release", + # "project_id":7, + # "created_at":"2016-03-08T09:14:35.023Z", + # "updated_at":"2016-03-08T09:14:36.351Z" + # } # ], - # "events": [ + # "events":[ # { - # "id": 1, - # "target_type": null, - # "target_id": null, - # "title": null, - # "data": null, - # "project_id": 1, - # "created_at": "2016-03-07T17:05:20.926Z", - # "updated_at": "2016-03-07T17:05:20.926Z", - # "action": 8, - # "author_id": 3 + # "id":1, + # "target_type":null, + # "target_id":null, + # "title":null, + # "data":null, + # "project_id":7, + # "created_at":"2016-03-08T09:14:36.806Z", + # "updated_at":"2016-03-08T09:14:36.806Z", + # "action":8, + # "author_id":1 # } # ], - # "commit_statuses": [ - # + # "commit_statuses":[ + # { + # "id":1, + # "project_id":null, + # "status":"success", + # "finished_at":"2016-01-26T07:23:42.000Z", + # "trace":null, + # "created_at":"2016-03-08T09:14:35.633Z", + # "updated_at":"2016-03-08T09:14:36.385Z", + # "started_at":"2016-01-26T07:21:42.000Z", + # "runner_id":null, + # "coverage":null, + # "commit_id":1, + # "commands":null, + # "job_id":null, + # "name":"default", + # "deploy":false, + # "options":null, + # "allow_failure":false, + # "stage":null, + # "trigger_request_id":null, + # "stage_idx":null, + # "tag":null, + # "ref":null, + # "user_id":null, + # "target_url":null, + # "description":"commit status", + # "artifacts_file":null, + # "gl_project_id":7, + # "artifacts_metadata":null, + # "erased_by_id":null, + # "erased_at":null + # } # ] # } end -- cgit v1.2.1 From 4e73f9827cc5b164e409a6beb9a305ef5604423d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 8 Mar 2016 12:35:37 +0100 Subject: WIP - started working on bundle the repo, refactored some stuff and updated spec --- .../projects/import_export/command_line_util.rb | 11 +++++++ .../projects/import_export/export_service.rb | 8 ++++- .../projects/import_export/project_tree_saver.rb | 11 +++---- .../projects/import_export/repo_bundler.rb | 37 ++++++++++++++++++++++ app/services/projects/import_export/shared.rb | 13 ++++++++ .../import_export/project_tree_saver_spec.rb | 3 +- 6 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 app/services/projects/import_export/command_line_util.rb create mode 100644 app/services/projects/import_export/repo_bundler.rb create mode 100644 app/services/projects/import_export/shared.rb diff --git a/app/services/projects/import_export/command_line_util.rb b/app/services/projects/import_export/command_line_util.rb new file mode 100644 index 00000000000..7b0cc08763b --- /dev/null +++ b/app/services/projects/import_export/command_line_util.rb @@ -0,0 +1,11 @@ +module Projects + module ImportExport + module CommandLineUtil + def tar_cf(archive:, dir:) + cmd = %W(tar -cf #{archive} -C #{dir} .) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end + end + end +end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index c89dd3d12ed..b3ad278733a 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,13 +2,19 @@ module Projects module ImportExport class ExportService < BaseService def execute(options = {}) + @shared = Projects::ImportExport::Shared.new(project_name: @project_name) save_project_tree + bundle_repo end private def save_project_tree - Projects::ImportExport::ProjectTreeSaver.save(project: project) + Projects::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save + end + + def bundle_repo + Projects::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle end end end diff --git a/app/services/projects/import_export/project_tree_saver.rb b/app/services/projects/import_export/project_tree_saver.rb index 4801c791ffe..3ca90d1f79a 100644 --- a/app/services/projects/import_export/project_tree_saver.rb +++ b/app/services/projects/import_export/project_tree_saver.rb @@ -3,19 +3,20 @@ module Projects class ProjectTreeSaver attr_reader :full_path - def initialize(project: ) + def initialize(project: , shared: ) @project = project + @export_path = shared.export_path end def save - @full_path = File.join(export_path, project_filename) + @full_path = File.join(@export_path, project_filename) save_to_disk end private def save_to_disk - FileUtils.mkdir_p(export_path) + FileUtils.mkdir_p(@export_path) File.write(full_path, project_json_tree) true rescue @@ -23,10 +24,6 @@ module Projects false end - def export_path - @export_path ||= ImportExport.export_path(@project.name) - end - def project_filename # TODO sanitize name "#{@project.name}.json" diff --git a/app/services/projects/import_export/repo_bundler.rb b/app/services/projects/import_export/repo_bundler.rb new file mode 100644 index 00000000000..27f1d5c87e5 --- /dev/null +++ b/app/services/projects/import_export/repo_bundler.rb @@ -0,0 +1,37 @@ +module Projects + module ImportExport + class RepoBundler + include Projects::ImportExport::CommandLineUtil + + attr_reader :full_path + + def initialize(project: , shared: ) + @project = project + @export_path = shared.export_path + end + + def bundle + return false if project.empty_repo? + @full_path = File.join(export_path, project_filename) + bundle_to_disk + end + + private + + def bundle_to_disk + tar_cf(archive: full_path, dir: path_to_repo) + rescue + #TODO: handle error + false + end + + def project_filename + @project.path_with_namespace + ".bundle" + end + + def path_to_repo + @project.repository.path_to_repo + end + end + end +end diff --git a/app/services/projects/import_export/shared.rb b/app/services/projects/import_export/shared.rb new file mode 100644 index 00000000000..cdff8b02dd3 --- /dev/null +++ b/app/services/projects/import_export/shared.rb @@ -0,0 +1,13 @@ +module Projects + module ImportExport + class Shared + def initialize(opts) + @opts = opts + end + + def export_path + @export_path ||= ImportExport.export_path(project_name: @opts[:project_name]) + end + end + end +end \ No newline at end of file diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 42302ffee8f..816171ca7e5 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -23,7 +23,8 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do end let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } - let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project) } + let(:shared) { Projects::ImportExport::Shared.new(project_name: @project_name) } + let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project, shared: shared) } before(:each) do project.team << [user, :master] -- cgit v1.2.1 From 556cafa44e7baae4de0f4169703d8a6174de458a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 8 Mar 2016 15:53:32 +0100 Subject: added repo bundler spec and refactored some of the export code --- app/services/projects/import_export.rb | 4 ++-- .../projects/import_export/export_service.rb | 2 +- .../projects/import_export/repo_bundler.rb | 7 +++--- app/services/projects/import_export/shared.rb | 2 +- .../import_export/project_tree_saver_spec.rb | 4 ++-- .../projects/import_export/repo_bundler_spec.rb | 25 ++++++++++++++++++++++ 6 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 spec/services/projects/import_export/repo_bundler_spec.rb diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index f87b10390a5..2fc0d5dd7fe 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -2,8 +2,8 @@ module Projects module ImportExport extend self - def export_path(project_name:) - File.join(storage_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export_#{project_name}") + def export_path(relative_path:) + File.join(storage_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export/#{relative_path}") end def project_atts diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index b3ad278733a..99aa8489f65 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,7 +2,7 @@ module Projects module ImportExport class ExportService < BaseService def execute(options = {}) - @shared = Projects::ImportExport::Shared.new(project_name: @project_name) + @shared = Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) save_project_tree bundle_repo end diff --git a/app/services/projects/import_export/repo_bundler.rb b/app/services/projects/import_export/repo_bundler.rb index 27f1d5c87e5..b0b2df1c14c 100644 --- a/app/services/projects/import_export/repo_bundler.rb +++ b/app/services/projects/import_export/repo_bundler.rb @@ -11,14 +11,15 @@ module Projects end def bundle - return false if project.empty_repo? - @full_path = File.join(export_path, project_filename) + return false if @project.empty_repo? + @full_path = File.join(@export_path, project_filename) bundle_to_disk end private def bundle_to_disk + FileUtils.mkdir_p(@export_path) tar_cf(archive: full_path, dir: path_to_repo) rescue #TODO: handle error @@ -26,7 +27,7 @@ module Projects end def project_filename - @project.path_with_namespace + ".bundle" + "#{@project.namespace}#{@project.name}.bundle" end def path_to_repo diff --git a/app/services/projects/import_export/shared.rb b/app/services/projects/import_export/shared.rb index cdff8b02dd3..5101f514ab2 100644 --- a/app/services/projects/import_export/shared.rb +++ b/app/services/projects/import_export/shared.rb @@ -6,7 +6,7 @@ module Projects end def export_path - @export_path ||= ImportExport.export_path(project_name: @opts[:project_name]) + @export_path ||= Projects::ImportExport.export_path(relative_path: @opts[:relative_path]) end end end diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 816171ca7e5..1b3568bb7a8 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -23,12 +23,12 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do end let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } - let(:shared) { Projects::ImportExport::Shared.new(project_name: @project_name) } + let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project, shared: shared) } before(:each) do project.team << [user, :master] - allow_any_instance_of(Projects::ImportExport::ProjectTreeSaver).to receive(:export_path).and_return(export_path) + allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) end after(:each) do diff --git a/spec/services/projects/import_export/repo_bundler_spec.rb b/spec/services/projects/import_export/repo_bundler_spec.rb new file mode 100644 index 00000000000..1f8ed41718f --- /dev/null +++ b/spec/services/projects/import_export/repo_bundler_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Projects::ImportExport::RepoBundler, services: true do + describe :bundle do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:bundler) { Projects::ImportExport::RepoBundler.new(project: project, shared: shared) } + + before(:each) do + project.team << [user, :master] + allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after(:each) do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(bundler.bundle).to be true + end + end +end -- cgit v1.2.1 From 99d87d8698437f87c21ced1d5a7fab4f9d253493 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 8 Mar 2016 18:17:57 +0100 Subject: WIP - added wiki repo bundler --- .../projects/import_export/command_line_util.rb | 6 +++++ .../projects/import_export/wiki_repo_bundler.rb | 30 ++++++++++++++++++++++ .../import_export/wiki_repo_bundler_spec.rb | 25 ++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 app/services/projects/import_export/wiki_repo_bundler.rb create mode 100644 spec/services/projects/import_export/wiki_repo_bundler_spec.rb diff --git a/app/services/projects/import_export/command_line_util.rb b/app/services/projects/import_export/command_line_util.rb index 7b0cc08763b..3ca49e76c7f 100644 --- a/app/services/projects/import_export/command_line_util.rb +++ b/app/services/projects/import_export/command_line_util.rb @@ -6,6 +6,12 @@ module Projects _output, status = Gitlab::Popen.popen(cmd) status.zero? end + + def git_bundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + cmd = %W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end end end end diff --git a/app/services/projects/import_export/wiki_repo_bundler.rb b/app/services/projects/import_export/wiki_repo_bundler.rb new file mode 100644 index 00000000000..793c8efd142 --- /dev/null +++ b/app/services/projects/import_export/wiki_repo_bundler.rb @@ -0,0 +1,30 @@ +module Projects + module ImportExport + class WikiRepoBundler < RepoBundler + def bundle + @wiki = ProjectWiki.new(@project) + return false if !wiki? + @full_path = File.join(@export_path, project_filename) + bundle_to_disk + end + + def bundle_to_disk + FileUtils.mkdir_p(@export_path) + git_bundle(repo_path: path_to_repo, bundle_path: @full_path) + rescue + #TODO: handle error + false + end + + private + + def path_to_repo + @wiki.repository.path_to_repo + end + + def wiki? + File.exists?(@wiki.repository.path_to_repo) && !@wiki.repository.empty? + end + end + end +end diff --git a/spec/services/projects/import_export/wiki_repo_bundler_spec.rb b/spec/services/projects/import_export/wiki_repo_bundler_spec.rb new file mode 100644 index 00000000000..351419d0619 --- /dev/null +++ b/spec/services/projects/import_export/wiki_repo_bundler_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Projects::ImportExport::WikiRepoBundler, services: true do + describe :bundle do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:wiki_bundler) { Projects::ImportExport::WikiRepoBundler.new(project: project, shared: shared) } + + before(:each) do + project.team << [user, :master] + allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after(:each) do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(wiki_bundler.bundle).to be true + end + end +end -- cgit v1.2.1 From f9c6168cc47496ac92e1a8c38dd9bd4ac39fd3d5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 8 Mar 2016 18:20:32 +0100 Subject: fix new line issue --- app/services/projects/import_export/shared.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/import_export/shared.rb b/app/services/projects/import_export/shared.rb index 5101f514ab2..04fe01c0d6c 100644 --- a/app/services/projects/import_export/shared.rb +++ b/app/services/projects/import_export/shared.rb @@ -10,4 +10,4 @@ module Projects end end end -end \ No newline at end of file +end -- cgit v1.2.1 From 4b88b4ffd5f49ea4cef189776783b58f7c6c141f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 9 Mar 2016 10:35:38 +0100 Subject: fix wiki path issues and spec --- app/services/projects/import_export/repo_bundler.rb | 2 +- app/services/projects/import_export/wiki_repo_bundler.rb | 4 ++++ spec/services/projects/import_export/wiki_repo_bundler_spec.rb | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/services/projects/import_export/repo_bundler.rb b/app/services/projects/import_export/repo_bundler.rb index b0b2df1c14c..d43fb4ea09e 100644 --- a/app/services/projects/import_export/repo_bundler.rb +++ b/app/services/projects/import_export/repo_bundler.rb @@ -27,7 +27,7 @@ module Projects end def project_filename - "#{@project.namespace}#{@project.name}.bundle" + "#{@project.name}.bundle" end def path_to_repo diff --git a/app/services/projects/import_export/wiki_repo_bundler.rb b/app/services/projects/import_export/wiki_repo_bundler.rb index 793c8efd142..bf69936503d 100644 --- a/app/services/projects/import_export/wiki_repo_bundler.rb +++ b/app/services/projects/import_export/wiki_repo_bundler.rb @@ -18,6 +18,10 @@ module Projects private + def project_filename + "#{@project.name}.wiki.bundle" + end + def path_to_repo @wiki.repository.path_to_repo end diff --git a/spec/services/projects/import_export/wiki_repo_bundler_spec.rb b/spec/services/projects/import_export/wiki_repo_bundler_spec.rb index 351419d0619..a589f81c5f9 100644 --- a/spec/services/projects/import_export/wiki_repo_bundler_spec.rb +++ b/spec/services/projects/import_export/wiki_repo_bundler_spec.rb @@ -8,10 +8,13 @@ describe Projects::ImportExport::WikiRepoBundler, services: true do let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:wiki_bundler) { Projects::ImportExport::WikiRepoBundler.new(project: project, shared: shared) } + let!(:project_wiki) { ProjectWiki.new(project, user) } before(:each) do project.team << [user, :master] allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) + project_wiki.wiki + project_wiki.create_page("index", "test content") end after(:each) do -- cgit v1.2.1 From 2dc2ce45d788e296a7b8c11417833573fcd721ea Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 9 Mar 2016 16:21:02 +0100 Subject: WIP - broken spec and some import stuff so I can create a MR on this. --- .../projects/import_export/import_service.rb | 19 ++++++++++++++ .../import_export/project_tree_restorer.rb | 30 ++++++++++++++++++++++ .../projects/import_export/relation_factory.rb | 18 +++++++++++++ fixtures/import_export/project.json | 1 + .../import_export/project_tree_restorer_spec.rb | 29 +++++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 app/services/projects/import_export/import_service.rb create mode 100644 app/services/projects/import_export/project_tree_restorer.rb create mode 100644 app/services/projects/import_export/relation_factory.rb create mode 100644 fixtures/import_export/project.json create mode 100644 spec/services/projects/import_export/project_tree_restorer_spec.rb diff --git a/app/services/projects/import_export/import_service.rb b/app/services/projects/import_export/import_service.rb new file mode 100644 index 00000000000..3efddf75e48 --- /dev/null +++ b/app/services/projects/import_export/import_service.rb @@ -0,0 +1,19 @@ +module Projects + module ImportExport + class ExportService < BaseService + def execute(options = {}) + @import_path = options[:import_path] + end + + private + + def restore_project_tree + Projects::ImportExport::ProjectTreeRestorer.new(path: @import_path).restore + end + + def restore_repo + + end + end + end +end diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb new file mode 100644 index 00000000000..c6aabc42f40 --- /dev/null +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -0,0 +1,30 @@ +module Projects + module ImportExport + class ProjectTreeRestorer + attr_reader :full_path + + def initialize(path: ) + @path = path + end + + def restore + json = IO.read(@path) + tree_hash = ActiveSupport::JSON.decode(json) + ImportExport.project_tree.each do |relation| + next if tree_hash[relation.to_s].empty? + tree_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s]) + end + project = Project.new(tree_hash) + project + end + + private + + def create_relation(relation, tree_hash) + Projects::ImportExport::RelationFactory.create( + relation_sym: relation, relation_hash: tree_hash[relation.to_s]) + end + + end + end +end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb new file mode 100644 index 00000000000..03fa36428c9 --- /dev/null +++ b/app/services/projects/import_export/relation_factory.rb @@ -0,0 +1,18 @@ +module Projects + module ImportExport + module RelationFactory + extend self + + def create(relation_sym: , relation_hash:) + klass = relation_class(relation_sym) + klass.new(relation_hash) + end + + private + + def relation_class(relation_sym) + relation_sym.to_s.classify.constantize + end + end + end +end diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json new file mode 100644 index 00000000000..8ea17e4d99c --- /dev/null +++ b/fixtures/import_export/project.json @@ -0,0 +1 @@ +{"id":14,"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Eos ut accusamus provident quis qui.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-09T11:41:00.754Z","updated_at":"2016-03-09T11:41:05.728Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Eos dolores molestias eum magni ut consequatur deleniti distinctio.","created_at":"2016-03-09T11:41:01.820Z","updated_at":"2016-03-09T11:41:01.820Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-09T11:41:02.959Z","updated_at":"2016-03-09T11:41:05.750Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-09T11:41:05.846Z","updated_at":"2016-03-09T11:41:05.846Z","state":"active","iid":1}],"snippets":[{"id":2,"title":"Unde adipisci molestiae modi quisquam qui itaque ut aut.","content":"Temporibus qui nisi ipsam maiores voluptatum.","author_id":30,"project_id":14,"created_at":"2016-03-09T11:41:03.720Z","updated_at":"2016-03-09T11:41:05.763Z","file_name":"sarah","expires_at":null,"visibility_level":0}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-09T11:41:04.342Z","updated_at":"2016-03-09T11:41:05.776Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-09T11:41:05.998Z","updated_at":"2016-03-09T11:41:05.998Z","action":8,"author_id":19}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-09T11:41:05.011Z","updated_at":"2016-03-09T11:41:05.801Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}]} \ No newline at end of file diff --git a/spec/services/projects/import_export/project_tree_restorer_spec.rb b/spec/services/projects/import_export/project_tree_restorer_spec.rb new file mode 100644 index 00000000000..f5b4340805c --- /dev/null +++ b/spec/services/projects/import_export/project_tree_restorer_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Projects::ImportExport::ProjectTreeRestorer, services: true do + describe :restore do + + let(:user) { create(:user) } + let(:project_tree_restorer) { Projects::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json") } + + before(:each) do + #allow(project_tree_restorer) + # .to receive(:full_path).and_return("fixtures/import_export/project.json") + end + + context 'JSON' do + let(:restored_project_json) do + project_tree_restorer.restore + #project_json(project_tree_restorer.full_path) + end + + it 'restores models based on JSON' do + expect(restored_project_json).to be true + end + end + end + + def project_json + JSON.parse(IO.read("fixtures/import_export/project.json")) + end +end -- cgit v1.2.1 From 2cc6aaec4910be1c37b4eeab028ffcdb4c8b9b7f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 9 Mar 2016 18:42:04 +0100 Subject: WIP (broken) - playing with import stuff --- .../import_export/project_tree_restorer.rb | 24 ++++++++++++++-------- .../projects/import_export/relation_factory.rb | 10 ++++++++- .../import_export/project_tree_restorer_spec.rb | 12 +---------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index c6aabc42f40..e30cb25ea61 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -1,30 +1,36 @@ module Projects module ImportExport class ProjectTreeRestorer - attr_reader :full_path + attr_reader :project - def initialize(path: ) + def initialize(path: , user: user) @path = path + @user = user end + #TODO deal with ID issues. + #TODO refactor this method def restore json = IO.read(@path) tree_hash = ActiveSupport::JSON.decode(json) + relation_hash = {} ImportExport.project_tree.each do |relation| next if tree_hash[relation.to_s].empty? - tree_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s]) + relation_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s]) end - project = Project.new(tree_hash) - project + project_params = tree_hash.delete_if { |_key, value | value.is_a?(Array)} + @project = ::Projects::CreateService.new(@user, project_params).execute + @project.saved? end private - def create_relation(relation, tree_hash) - Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: tree_hash[relation.to_s]) + def create_relation(relation, relation_hash_list) + relation_hash_list.map do |relation_hash| + Projects::ImportExport::RelationFactory.create( + relation_sym: relation, relation_hash: relation_hash) + end end - end end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 03fa36428c9..9f4bc7b99ec 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -3,8 +3,12 @@ module Projects module RelationFactory extend self - def create(relation_sym: , relation_hash:) + OVERRIDES = { snippets: :project_snippets } + + def create(relation_sym:, relation_hash:) + relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) + relation_hash.delete('id') #screw IDs for now klass.new(relation_hash) end @@ -13,6 +17,10 @@ module Projects def relation_class(relation_sym) relation_sym.to_s.classify.constantize end + + def parse_relation_sym(relation_sym) + OVERRIDES[relation_sym] || relation_sym + end end end end diff --git a/spec/services/projects/import_export/project_tree_restorer_spec.rb b/spec/services/projects/import_export/project_tree_restorer_spec.rb index f5b4340805c..4c8d182a213 100644 --- a/spec/services/projects/import_export/project_tree_restorer_spec.rb +++ b/spec/services/projects/import_export/project_tree_restorer_spec.rb @@ -4,17 +4,11 @@ describe Projects::ImportExport::ProjectTreeRestorer, services: true do describe :restore do let(:user) { create(:user) } - let(:project_tree_restorer) { Projects::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json") } - - before(:each) do - #allow(project_tree_restorer) - # .to receive(:full_path).and_return("fixtures/import_export/project.json") - end + let(:project_tree_restorer) { Projects::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json", user: user) } context 'JSON' do let(:restored_project_json) do project_tree_restorer.restore - #project_json(project_tree_restorer.full_path) end it 'restores models based on JSON' do @@ -22,8 +16,4 @@ describe Projects::ImportExport::ProjectTreeRestorer, services: true do end end end - - def project_json - JSON.parse(IO.read("fixtures/import_export/project.json")) - end end -- cgit v1.2.1 From f0ce83b1902e78b8adc819db49022ed98d12342e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 10 Mar 2016 13:04:11 +0100 Subject: modify model creation to use services when they are available --- .../import_export/project_tree_restorer.rb | 6 ++-- .../projects/import_export/relation_factory.rb | 39 ++++++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index e30cb25ea61..161081bd75d 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -14,12 +14,12 @@ module Projects json = IO.read(@path) tree_hash = ActiveSupport::JSON.decode(json) relation_hash = {} + project_params = tree_hash.reject { |_key, value | value.is_a?(Array)} + @project = ::Projects::CreateService.new(@user, project_params.except('id')).execute ImportExport.project_tree.each do |relation| next if tree_hash[relation.to_s].empty? relation_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s]) end - project_params = tree_hash.delete_if { |_key, value | value.is_a?(Array)} - @project = ::Projects::CreateService.new(@user, project_params).execute @project.saved? end @@ -28,7 +28,7 @@ module Projects def create_relation(relation, relation_hash_list) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash) + relation_sym: relation, relation_hash: relation_hash, project: @project, user: @user) end end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 9f4bc7b99ec..f27903ef10f 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -1,24 +1,43 @@ module Projects module ImportExport - module RelationFactory - extend self + class RelationFactory OVERRIDES = { snippets: :project_snippets } - def create(relation_sym:, relation_hash:) - relation_sym = parse_relation_sym(relation_sym) - klass = relation_class(relation_sym) - relation_hash.delete('id') #screw IDs for now - klass.new(relation_hash) + def self.create(*args) + new(*args).create + end + + def initialize(relation_sym:, relation_hash:, project:, user:) + @relation_sym = parsed_relation_sym(relation_sym) + @relation_hash = relation_hash + @project = project + @user = user + end + + def create + @relation_hash.delete('id') + init_service_or_class end private - def relation_class(relation_sym) - relation_sym.to_s.classify.constantize + def init_service_or_class + # Attempt service first + relation_service.new(@project, @user, @relation_hash).execute + rescue NameError + relation_class.new(@relation_hash) + end + + def relation_service + "#{@relation_sym.to_s.classify}::CreateService".constantize + end + + def relation_class + @relation_sym.to_s.classify.constantize end - def parse_relation_sym(relation_sym) + def parsed_relation_sym(relation_sym) OVERRIDES[relation_sym] || relation_sym end end -- cgit v1.2.1 From 40de1b46342ce11668498d725571d253ff99172e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 9 Mar 2016 18:42:04 +0100 Subject: revert back services changes (cherry picked from commit 2cc6aae) --- .../import_export/project_tree_restorer.rb | 6 ++-- .../projects/import_export/relation_factory.rb | 39 ++++++---------------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 161081bd75d..e30cb25ea61 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -14,12 +14,12 @@ module Projects json = IO.read(@path) tree_hash = ActiveSupport::JSON.decode(json) relation_hash = {} - project_params = tree_hash.reject { |_key, value | value.is_a?(Array)} - @project = ::Projects::CreateService.new(@user, project_params.except('id')).execute ImportExport.project_tree.each do |relation| next if tree_hash[relation.to_s].empty? relation_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s]) end + project_params = tree_hash.delete_if { |_key, value | value.is_a?(Array)} + @project = ::Projects::CreateService.new(@user, project_params).execute @project.saved? end @@ -28,7 +28,7 @@ module Projects def create_relation(relation, relation_hash_list) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash, project: @project, user: @user) + relation_sym: relation, relation_hash: relation_hash) end end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index f27903ef10f..9f4bc7b99ec 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -1,43 +1,24 @@ module Projects module ImportExport - class RelationFactory + module RelationFactory + extend self OVERRIDES = { snippets: :project_snippets } - def self.create(*args) - new(*args).create - end - - def initialize(relation_sym:, relation_hash:, project:, user:) - @relation_sym = parsed_relation_sym(relation_sym) - @relation_hash = relation_hash - @project = project - @user = user - end - - def create - @relation_hash.delete('id') - init_service_or_class + def create(relation_sym:, relation_hash:) + relation_sym = parse_relation_sym(relation_sym) + klass = relation_class(relation_sym) + relation_hash.delete('id') #screw IDs for now + klass.new(relation_hash) end private - def init_service_or_class - # Attempt service first - relation_service.new(@project, @user, @relation_hash).execute - rescue NameError - relation_class.new(@relation_hash) - end - - def relation_service - "#{@relation_sym.to_s.classify}::CreateService".constantize - end - - def relation_class - @relation_sym.to_s.classify.constantize + def relation_class(relation_sym) + relation_sym.to_s.classify.constantize end - def parsed_relation_sym(relation_sym) + def parse_relation_sym(relation_sym) OVERRIDES[relation_sym] || relation_sym end end -- cgit v1.2.1 From e90b1e147e69fb05bd10c2a9b4198af7ee386eb1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 10 Mar 2016 15:35:44 +0100 Subject: refactored project creation to create the associations as well --- .../projects/import_export/project_factory.rb | 40 ++++++++++++++++++++++ .../import_export/project_tree_restorer.rb | 15 ++++---- 2 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 app/services/projects/import_export/project_factory.rb diff --git a/app/services/projects/import_export/project_factory.rb b/app/services/projects/import_export/project_factory.rb new file mode 100644 index 00000000000..1ca0cfb8673 --- /dev/null +++ b/app/services/projects/import_export/project_factory.rb @@ -0,0 +1,40 @@ +module Projects + module ImportExport + module ProjectFactory + extend self + + def create(project_params:, user:) + project = Project.new(project_params.except('id')) + project.creator = user + check_namespace(project_params['namespace_id'], project, user) + end + + def check_namespace(namespace_id, project, user) + if namespace_id + # Find matching namespace and check if it allowed + # for current user if namespace_id passed. + unless allowed_namespace?(user, namespace_id) + project.namespace_id = nil + deny_namespace(project) + end + else + # Set current user namespace if namespace_id is nil + project.namespace_id = user.namespace_id + end + project + end + + private + + def allowed_namespace?(user, namespace_id) + namespace = Namespace.find_by(id: namespace_id) + user.can?(:create_projects, namespace) + end + + def deny_namespace(project) + project.errors.add(:namespace, "is not valid") + end + + end + end +end diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index e30cb25ea61..980a376be16 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -3,7 +3,7 @@ module Projects class ProjectTreeRestorer attr_reader :project - def initialize(path: , user: user) + def initialize(path:, user:) @path = path @user = user end @@ -13,22 +13,23 @@ module Projects def restore json = IO.read(@path) tree_hash = ActiveSupport::JSON.decode(json) + project_params = tree_hash.reject { |_key, value| value.is_a?(Array) } + project = Projects::ImportExport::ProjectFactory.create(project_params: project_params, user: @user) + project.save relation_hash = {} ImportExport.project_tree.each do |relation| next if tree_hash[relation.to_s].empty? - relation_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s]) + relation_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s], project.id) + project.update_attribute(relation, relation_hash[relation.to_s]) end - project_params = tree_hash.delete_if { |_key, value | value.is_a?(Array)} - @project = ::Projects::CreateService.new(@user, project_params).execute - @project.saved? end private - def create_relation(relation, relation_hash_list) + def create_relation(relation, relation_hash_list, project_id) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash) + relation_sym: relation, relation_hash: relation_hash.merge(project_id: project_id)) end end end -- cgit v1.2.1 From 2a7a1bcc3aa606a0aab46620c2b262a33c276c7f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 10 Mar 2016 16:21:17 +0100 Subject: refactored methods in project tree restorer --- .../projects/import_export/project_tree_restorer.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 980a376be16..3ad42f41457 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -13,19 +13,23 @@ module Projects def restore json = IO.read(@path) tree_hash = ActiveSupport::JSON.decode(json) - project_params = tree_hash.reject { |_key, value| value.is_a?(Array) } - project = Projects::ImportExport::ProjectFactory.create(project_params: project_params, user: @user) - project.save - relation_hash = {} + project = create_project(tree_hash) ImportExport.project_tree.each do |relation| next if tree_hash[relation.to_s].empty? - relation_hash[relation.to_s] = create_relation(relation, tree_hash[relation.to_s], project.id) - project.update_attribute(relation, relation_hash[relation.to_s]) + relation_hash = create_relation(relation, tree_hash[relation.to_s], project.id) + project.update_attribute(relation, relation_hash) end end private + def create_project(tree_hash) + project_params = tree_hash.reject { |_key, value| value.is_a?(Array) } + project = Projects::ImportExport::ProjectFactory.create(project_params: project_params, user: @user) + project.save + project + end + def create_relation(relation, relation_hash_list, project_id) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( -- cgit v1.2.1 From a7acfa6614c0645f61a070814e0250b8fbe0cf78 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 10 Mar 2016 16:29:38 +0100 Subject: add project members to export --- app/services/projects/import_export.rb | 2 +- spec/services/projects/import_export/project_tree_saver_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index 2fc0d5dd7fe..601762e00e2 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -7,7 +7,7 @@ module Projects end def project_atts - %i(id name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) + %i(project_members name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) end def project_tree diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 1b3568bb7a8..839e1b76692 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -81,6 +81,10 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do it 'has commit statuses' do expect(saved_project_json['commit_statuses']).not_to be_empty end + + it 'has project members' do + expect(saved_project_json['commit_statuses']).not_to be_empty + end end end -- cgit v1.2.1 From cc8aafeded77cf09580fe3a9f4718dd1b7d33c19 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 10 Mar 2016 18:41:57 +0100 Subject: typo --- spec/services/projects/import_export/project_tree_saver_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 839e1b76692..383b3e4ce8b 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -83,7 +83,7 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do end it 'has project members' do - expect(saved_project_json['commit_statuses']).not_to be_empty + expect(saved_project_json['project_members']).not_to be_empty end end end -- cgit v1.2.1 From febff153cc850ef662301dd2a6b19df91186477f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 10 Mar 2016 18:43:57 +0100 Subject: started mapping members --- .../projects/import_export/members_mapper.rb | 18 +++++++++++ .../projects/import_export/project_factory.rb | 2 +- .../import_export/project_tree_restorer.rb | 36 ++++++++++++++-------- fixtures/import_export/project.json | 2 +- 4 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 app/services/projects/import_export/members_mapper.rb diff --git a/app/services/projects/import_export/members_mapper.rb b/app/services/projects/import_export/members_mapper.rb new file mode 100644 index 00000000000..ebb50c30b84 --- /dev/null +++ b/app/services/projects/import_export/members_mapper.rb @@ -0,0 +1,18 @@ +module Projects + module ImportExport + class MembersMapper + + def self.map(*args) + new(*args).map + end + + def initialize(exported_members:) + @exported_members = exported_members + end + + def map + #TODO + end + end + end +end diff --git a/app/services/projects/import_export/project_factory.rb b/app/services/projects/import_export/project_factory.rb index 1ca0cfb8673..2df38bff1f8 100644 --- a/app/services/projects/import_export/project_factory.rb +++ b/app/services/projects/import_export/project_factory.rb @@ -3,7 +3,7 @@ module Projects module ProjectFactory extend self - def create(project_params:, user:) + def create(project_params:, user:, members:) project = Project.new(project_params.except('id')) project.creator = user check_namespace(project_params['namespace_id'], project, user) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 3ad42f41457..8785c80e241 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -8,32 +8,42 @@ module Projects @user = user end - #TODO deal with ID issues. - #TODO refactor this method def restore json = IO.read(@path) - tree_hash = ActiveSupport::JSON.decode(json) - project = create_project(tree_hash) - ImportExport.project_tree.each do |relation| - next if tree_hash[relation.to_s].empty? - relation_hash = create_relation(relation, tree_hash[relation.to_s], project.id) + @tree_hash = ActiveSupport::JSON.decode(json) + create_relations + end + + private + + def members + @members ||= Projects::ImportExport::MembersMapper.map(exported_members: @tree_hash.delete('project_members')) + end + + def create_relations + (ImportExport.project_tree - [:project_members]).each do |relation| + next if @tree_hash[relation.to_s].empty? + relation_hash = create_relation(relation, @tree_hash[relation.to_s]) project.update_attribute(relation, relation_hash) end end - private + def project + @project ||= create_project + end - def create_project(tree_hash) - project_params = tree_hash.reject { |_key, value| value.is_a?(Array) } - project = Projects::ImportExport::ProjectFactory.create(project_params: project_params, user: @user) + def create_project + project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } + project = Projects::ImportExport::ProjectFactory.create( + project_params: project_params, user: @user, members: members) project.save project end - def create_relation(relation, relation_hash_list, project_id) + def create_relation(relation, relation_hash_list) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash.merge(project_id: project_id)) + relation_sym: relation, relation_hash: relation_hash.merge(project_id: project.id), members: members) end end end diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json index 8ea17e4d99c..fe8bb17e6c3 100644 --- a/fixtures/import_export/project.json +++ b/fixtures/import_export/project.json @@ -1 +1 @@ -{"id":14,"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Eos ut accusamus provident quis qui.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-09T11:41:00.754Z","updated_at":"2016-03-09T11:41:05.728Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Eos dolores molestias eum magni ut consequatur deleniti distinctio.","created_at":"2016-03-09T11:41:01.820Z","updated_at":"2016-03-09T11:41:01.820Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-09T11:41:02.959Z","updated_at":"2016-03-09T11:41:05.750Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-09T11:41:05.846Z","updated_at":"2016-03-09T11:41:05.846Z","state":"active","iid":1}],"snippets":[{"id":2,"title":"Unde adipisci molestiae modi quisquam qui itaque ut aut.","content":"Temporibus qui nisi ipsam maiores voluptatum.","author_id":30,"project_id":14,"created_at":"2016-03-09T11:41:03.720Z","updated_at":"2016-03-09T11:41:05.763Z","file_name":"sarah","expires_at":null,"visibility_level":0}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-09T11:41:04.342Z","updated_at":"2016-03-09T11:41:05.776Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-09T11:41:05.998Z","updated_at":"2016-03-09T11:41:05.998Z","action":8,"author_id":19}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-09T11:41:05.011Z","updated_at":"2016-03-09T11:41:05.801Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}]} \ No newline at end of file +{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Culpa dolores et nostrum magni quo.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-10T15:54:30.960Z","updated_at":"2016-03-10T15:54:36.624Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Distinctio voluptas qui suscipit asperiores.","created_at":"2016-03-10T15:54:31.921Z","updated_at":"2016-03-10T15:54:31.921Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-10T15:54:33.128Z","updated_at":"2016-03-10T15:54:36.645Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-10T15:54:36.756Z","updated_at":"2016-03-10T15:54:36.756Z","state":"active","iid":1}],"snippets":[{"id":2,"title":"In voluptatibus possimus fugiat qui.","content":"Qui exercitationem culpa in dolore ab.","author_id":30,"project_id":14,"created_at":"2016-03-10T15:54:34.253Z","updated_at":"2016-03-10T15:54:36.658Z","file_name":"patrick","expires_at":null,"visibility_level":0}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-10T15:54:35.211Z","updated_at":"2016-03-10T15:54:36.671Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-10T15:54:36.906Z","updated_at":"2016-03-10T15:54:36.906Z","action":8,"author_id":19}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-10T15:54:35.937Z","updated_at":"2016-03-10T15:54:36.699Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}]} \ No newline at end of file -- cgit v1.2.1 From 2fd30231867226d8c921ec74f388e6f30769e2a8 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 09:46:00 +0100 Subject: fix project members export --- app/services/projects/import_export.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index 601762e00e2..99336487ebc 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -7,11 +7,11 @@ module Projects end def project_atts - %i(project_members name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) + %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) end def project_tree - %i(issues merge_requests labels milestones snippets releases events commit_statuses) + %i(project_members issues merge_requests labels milestones snippets releases events commit_statuses) end private -- cgit v1.2.1 From b7149266eac2277108757d34cce05eb372d562ab Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 10:25:34 +0100 Subject: WIP project members - json not quite right yet --- app/services/projects/import_export/project_factory.rb | 2 +- app/services/projects/import_export/project_tree_restorer.rb | 6 +++--- app/services/projects/import_export/relation_factory.rb | 2 +- fixtures/import_export/project.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/projects/import_export/project_factory.rb b/app/services/projects/import_export/project_factory.rb index 2df38bff1f8..825907df537 100644 --- a/app/services/projects/import_export/project_factory.rb +++ b/app/services/projects/import_export/project_factory.rb @@ -3,7 +3,7 @@ module Projects module ProjectFactory extend self - def create(project_params:, user:, members:) + def create(project_params:, user:, members_map:) project = Project.new(project_params.except('id')) project.creator = user check_namespace(project_params['namespace_id'], project, user) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 8785c80e241..64ac3a534e2 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -16,7 +16,7 @@ module Projects private - def members + def members_map @members ||= Projects::ImportExport::MembersMapper.map(exported_members: @tree_hash.delete('project_members')) end @@ -35,7 +35,7 @@ module Projects def create_project project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project = Projects::ImportExport::ProjectFactory.create( - project_params: project_params, user: @user, members: members) + project_params: project_params, user: @user, members_map: members_map) project.save project end @@ -43,7 +43,7 @@ module Projects def create_relation(relation, relation_hash_list) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash.merge(project_id: project.id), members: members) + relation_sym: relation, relation_hash: relation_hash.merge(project_id: project.id), members_map: members_map) end end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 9f4bc7b99ec..624453a6ade 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -5,7 +5,7 @@ module Projects OVERRIDES = { snippets: :project_snippets } - def create(relation_sym:, relation_hash:) + def create(relation_sym:, relation_hash:, members_map:) relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') #screw IDs for now diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json index fe8bb17e6c3..5eaaf1cd8af 100644 --- a/fixtures/import_export/project.json +++ b/fixtures/import_export/project.json @@ -1 +1 @@ -{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Culpa dolores et nostrum magni quo.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-10T15:54:30.960Z","updated_at":"2016-03-10T15:54:36.624Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Distinctio voluptas qui suscipit asperiores.","created_at":"2016-03-10T15:54:31.921Z","updated_at":"2016-03-10T15:54:31.921Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-10T15:54:33.128Z","updated_at":"2016-03-10T15:54:36.645Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-10T15:54:36.756Z","updated_at":"2016-03-10T15:54:36.756Z","state":"active","iid":1}],"snippets":[{"id":2,"title":"In voluptatibus possimus fugiat qui.","content":"Qui exercitationem culpa in dolore ab.","author_id":30,"project_id":14,"created_at":"2016-03-10T15:54:34.253Z","updated_at":"2016-03-10T15:54:36.658Z","file_name":"patrick","expires_at":null,"visibility_level":0}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-10T15:54:35.211Z","updated_at":"2016-03-10T15:54:36.671Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-10T15:54:36.906Z","updated_at":"2016-03-10T15:54:36.906Z","action":8,"author_id":19}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-10T15:54:35.937Z","updated_at":"2016-03-10T15:54:36.699Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}]} \ No newline at end of file +{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"project_members":[{"id":11,"access_level":40,"source_id":77,"source_type":"Project","user_id":181,"notification_level":3,"created_at":"2016-03-11T09:02:34.007Z","updated_at":"2016-03-11T09:02:34.007Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null}],"issues":[{"id":11,"title":"Voluptatibus eius eaque qui officiis fugit.","assignee_id":181,"author_id":182,"project_id":77,"created_at":"2016-03-11T09:02:27.677Z","updated_at":"2016-03-11T09:02:33.728Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":11,"target_branch":"feature","source_branch":"master","source_project_id":72,"author_id":185,"assignee_id":null,"title":"Dolorum similique voluptate odio id unde a maxime officiis.","created_at":"2016-03-11T09:02:28.616Z","updated_at":"2016-03-11T09:02:28.616Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":77,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":11,"title":"Bug","color":"#990000","project_id":77,"created_at":"2016-03-11T09:02:29.974Z","updated_at":"2016-03-11T09:02:33.752Z","template":false,"description":null}],"milestones":[{"id":11,"title":"Milestone v1.2","project_id":77,"description":null,"due_date":null,"created_at":"2016-03-11T09:02:33.882Z","updated_at":"2016-03-11T09:02:33.882Z","state":"active","iid":1}],"snippets":[{"id":11,"title":"Voluptas et expedita autem quia totam voluptate culpa quis.","content":"Veritatis esse fugiat sint eos.","author_id":192,"project_id":77,"created_at":"2016-03-11T09:02:31.022Z","updated_at":"2016-03-11T09:02:33.768Z","file_name":"dexter.hessel","expires_at":null,"visibility_level":0}],"releases":[{"id":11,"tag":"v1.1.0","description":"Awesome release","project_id":77,"created_at":"2016-03-11T09:02:32.099Z","updated_at":"2016-03-11T09:02:33.787Z"}],"events":[{"id":11,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":77,"created_at":"2016-03-11T09:02:34.069Z","updated_at":"2016-03-11T09:02:34.069Z","action":8,"author_id":181}],"commit_statuses":[{"id":11,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-11T09:02:32.916Z","updated_at":"2016-03-11T09:02:33.825Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":11,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":77,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}]} \ No newline at end of file -- cgit v1.2.1 From eb2b9e3efc335b07ade0a44b22af6214089c528b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 11:23:59 +0100 Subject: fix project members json to include user --- app/services/projects/import_export.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index 99336487ebc..e281925fe9c 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -11,11 +11,15 @@ module Projects end def project_tree - %i(project_members issues merge_requests labels milestones snippets releases events commit_statuses) + %i(issues merge_requests labels milestones snippets releases events commit_statuses) + members end private + def members + [{ project_members: { include: [user: { only: [:email, :username] }] } }] + end + def storage_path File.join(Settings.shared['path'], 'tmp/project_exports') end -- cgit v1.2.1 From cbae4038acc2ac1d4644aa719d11ecbf368ce06c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 11:37:42 +0100 Subject: updated test json --- fixtures/import_export/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json index 5eaaf1cd8af..38c999e8fb1 100644 --- a/fixtures/import_export/project.json +++ b/fixtures/import_export/project.json @@ -1 +1 @@ -{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"project_members":[{"id":11,"access_level":40,"source_id":77,"source_type":"Project","user_id":181,"notification_level":3,"created_at":"2016-03-11T09:02:34.007Z","updated_at":"2016-03-11T09:02:34.007Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null}],"issues":[{"id":11,"title":"Voluptatibus eius eaque qui officiis fugit.","assignee_id":181,"author_id":182,"project_id":77,"created_at":"2016-03-11T09:02:27.677Z","updated_at":"2016-03-11T09:02:33.728Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":11,"target_branch":"feature","source_branch":"master","source_project_id":72,"author_id":185,"assignee_id":null,"title":"Dolorum similique voluptate odio id unde a maxime officiis.","created_at":"2016-03-11T09:02:28.616Z","updated_at":"2016-03-11T09:02:28.616Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":77,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":11,"title":"Bug","color":"#990000","project_id":77,"created_at":"2016-03-11T09:02:29.974Z","updated_at":"2016-03-11T09:02:33.752Z","template":false,"description":null}],"milestones":[{"id":11,"title":"Milestone v1.2","project_id":77,"description":null,"due_date":null,"created_at":"2016-03-11T09:02:33.882Z","updated_at":"2016-03-11T09:02:33.882Z","state":"active","iid":1}],"snippets":[{"id":11,"title":"Voluptas et expedita autem quia totam voluptate culpa quis.","content":"Veritatis esse fugiat sint eos.","author_id":192,"project_id":77,"created_at":"2016-03-11T09:02:31.022Z","updated_at":"2016-03-11T09:02:33.768Z","file_name":"dexter.hessel","expires_at":null,"visibility_level":0}],"releases":[{"id":11,"tag":"v1.1.0","description":"Awesome release","project_id":77,"created_at":"2016-03-11T09:02:32.099Z","updated_at":"2016-03-11T09:02:33.787Z"}],"events":[{"id":11,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":77,"created_at":"2016-03-11T09:02:34.069Z","updated_at":"2016-03-11T09:02:34.069Z","action":8,"author_id":181}],"commit_statuses":[{"id":11,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-11T09:02:32.916Z","updated_at":"2016-03-11T09:02:33.825Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":11,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":77,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}]} \ No newline at end of file +{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Voluptatem quia repudiandae id rem ducimus.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-11T10:21:37.525Z","updated_at":"2016-03-11T10:21:44.516Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Enim dolor veritatis suscipit sed laboriosam consequatur doloremque.","created_at":"2016-03-11T10:21:38.628Z","updated_at":"2016-03-11T10:21:38.628Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-11T10:21:39.964Z","updated_at":"2016-03-11T10:21:44.539Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-11T10:21:44.688Z","updated_at":"2016-03-11T10:21:44.688Z","state":"active","iid":1}],"snippets":[{"id":2,"title":"Dolorem perspiciatis unde aut similique aperiam totam qui iste.","content":"Sed similique praesentium tempora et ad dolor quis ut.","author_id":30,"project_id":14,"created_at":"2016-03-11T10:21:41.034Z","updated_at":"2016-03-11T10:21:44.557Z","file_name":"dee","expires_at":null,"visibility_level":0}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-11T10:21:42.531Z","updated_at":"2016-03-11T10:21:44.576Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-11T10:21:44.892Z","updated_at":"2016-03-11T10:21:44.892Z","action":8,"author_id":19}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-11T10:21:43.493Z","updated_at":"2016-03-11T10:21:44.620Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}],"project_members":[{"id":2,"access_level":40,"source_id":14,"source_type":"Project","user_id":19,"notification_level":3,"created_at":"2016-03-11T10:21:44.822Z","updated_at":"2016-03-11T10:21:44.822Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"user":{"email":"claudie.lemke@prohaskamarquardt.co.uk","username":"hiram_conroy19"}}]} \ No newline at end of file -- cgit v1.2.1 From 7b855b75f2699f3189610886d8b2acf7912bf7ef Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 12:55:50 +0100 Subject: project members import stuff --- .../projects/import_export/members_mapper.rb | 47 +++++++++++++++++++++- .../projects/import_export/project_factory.rb | 2 +- .../import_export/project_tree_restorer.rb | 5 ++- .../projects/import_export/members_mapper_spec.rb | 42 +++++++++++++++++++ 4 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 spec/services/projects/import_export/members_mapper_spec.rb diff --git a/app/services/projects/import_export/members_mapper.rb b/app/services/projects/import_export/members_mapper.rb index ebb50c30b84..56abc131b16 100644 --- a/app/services/projects/import_export/members_mapper.rb +++ b/app/services/projects/import_export/members_mapper.rb @@ -6,12 +6,55 @@ module Projects new(*args).map end - def initialize(exported_members:) + def initialize(exported_members:, user:, project_id:) @exported_members = exported_members + @user = user + @project_id = project_id end def map - #TODO + @project_member_map ||= project_member_map + end + + private + + def project_member_map + @project_member_map = Hash.new(default_project_member) + @exported_members.each do |member| + existing_user = User.where(find_project_user_query(member)).first + assign_member(existing_user, member) if existing_user + end + @project_member_map + end + + def assign_member(existing_user, member) + member['user'] = existing_user + project_member = ProjectMember.new(member_hash(member)) + @project_member_map[member['id']] = project_member if project_member.save + end + + def member_hash(member) + member.except('id').merge(source_id: @project_id) + end + + def default_project_member + @default_project_member ||= + begin + default_member = ProjectMember.new(default_project_member_hash) + default_member if default_member.save + end + end + + def default_project_member_hash + { user: @user, access_level: ProjectMember::MASTER, source_id: @project_id } + end + + def find_project_user_query(member) + user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email'])) + end + + def user_arel + @user_arel ||= User.arel_table end end end diff --git a/app/services/projects/import_export/project_factory.rb b/app/services/projects/import_export/project_factory.rb index 825907df537..1ca0cfb8673 100644 --- a/app/services/projects/import_export/project_factory.rb +++ b/app/services/projects/import_export/project_factory.rb @@ -3,7 +3,7 @@ module Projects module ProjectFactory extend self - def create(project_params:, user:, members_map:) + def create(project_params:, user:) project = Project.new(project_params.except('id')) project.creator = user check_namespace(project_params['namespace_id'], project, user) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 64ac3a534e2..87b78197cd6 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -17,7 +17,8 @@ module Projects private def members_map - @members ||= Projects::ImportExport::MembersMapper.map(exported_members: @tree_hash.delete('project_members')) + @members ||= Projects::ImportExport::MembersMapper.map( + exported_members: @tree_hash.delete('project_members'), user: @user, project_id: project.id) end def create_relations @@ -35,7 +36,7 @@ module Projects def create_project project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project = Projects::ImportExport::ProjectFactory.create( - project_params: project_params, user: @user, members_map: members_map) + project_params: project_params, user: @user) project.save project end diff --git a/spec/services/projects/import_export/members_mapper_spec.rb b/spec/services/projects/import_export/members_mapper_spec.rb new file mode 100644 index 00000000000..431626d5e78 --- /dev/null +++ b/spec/services/projects/import_export/members_mapper_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Projects::ImportExport::MembersMapper, services: true do + describe :map do + + let(:user) { create(:user) } + let(:project) { create(:project, :public, name: 'searchable_project') } + let(:user2) { create(:user) } + let(:exported_members) { [ + { + "id" => 2, + "access_level" => 40, + "source_id" => 14, + "source_type" => "Project", + "user_id" => 19, + "notification_level" => 3, + "created_at" => "2016-03-11T10:21:44.822Z", + "updated_at" => "2016-03-11T10:21:44.822Z", + "created_by_id" => nil, + "invite_email" => nil, + "invite_token" => nil, + "invite_accepted_at" => nil, + "user" => { "email" => user2.email, "username" => user2.username } }] } + + let(:members_mapper) do + Projects::ImportExport::MembersMapper.new( + exported_members: exported_members, user: user, project_id: project.id) + end + + it 'maps a project member' do + expect(project_member_map_id(user2.id)).to eq(user2.id) + end + + it 'defaults to importer project member if it does not exist' do + expect(project_member_map_id(-1)).to eq(user.id) + end + end + + def project_member_map_id(id) + members_mapper.map[id]['id'] + end +end -- cgit v1.2.1 From 18d3b38e464e3b8c759401b8a8fe643313931152 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 15:14:30 +0100 Subject: project members: fixed a few issues. Specs now passing --- .../projects/import_export/members_mapper.rb | 3 +- .../projects/import_export/members_mapper_spec.rb | 44 ++++++++++++---------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/services/projects/import_export/members_mapper.rb b/app/services/projects/import_export/members_mapper.rb index 56abc131b16..634683da5ce 100644 --- a/app/services/projects/import_export/members_mapper.rb +++ b/app/services/projects/import_export/members_mapper.rb @@ -30,13 +30,14 @@ module Projects def assign_member(existing_user, member) member['user'] = existing_user project_member = ProjectMember.new(member_hash(member)) - @project_member_map[member['id']] = project_member if project_member.save + @project_member_map[existing_user.id] = project_member if project_member.save end def member_hash(member) member.except('id').merge(source_id: @project_id) end + #TODO: If default, then we need to leave a comment 'Comment by ' on comments def default_project_member @default_project_member ||= begin diff --git a/spec/services/projects/import_export/members_mapper_spec.rb b/spec/services/projects/import_export/members_mapper_spec.rb index 431626d5e78..dc6ccb50121 100644 --- a/spec/services/projects/import_export/members_mapper_spec.rb +++ b/spec/services/projects/import_export/members_mapper_spec.rb @@ -6,21 +6,27 @@ describe Projects::ImportExport::MembersMapper, services: true do let(:user) { create(:user) } let(:project) { create(:project, :public, name: 'searchable_project') } let(:user2) { create(:user) } - let(:exported_members) { [ - { - "id" => 2, - "access_level" => 40, - "source_id" => 14, - "source_type" => "Project", - "user_id" => 19, - "notification_level" => 3, - "created_at" => "2016-03-11T10:21:44.822Z", - "updated_at" => "2016-03-11T10:21:44.822Z", - "created_by_id" => nil, - "invite_email" => nil, - "invite_token" => nil, - "invite_accepted_at" => nil, - "user" => { "email" => user2.email, "username" => user2.username } }] } + let(:exported_members) do + [{ + "id" => 2, + "access_level" => 40, + "source_id" => 14, + "source_type" => "Project", + "user_id" => 19, + "notification_level" => 3, + "created_at" => "2016-03-11T10:21:44.822Z", + "updated_at" => "2016-03-11T10:21:44.822Z", + "created_by_id" => nil, + "invite_email" => nil, + "invite_token" => nil, + "invite_accepted_at" => nil, + "user" => + { + "email" => user2.email, + "username" => user2.username + } + }] + end let(:members_mapper) do Projects::ImportExport::MembersMapper.new( @@ -28,15 +34,15 @@ describe Projects::ImportExport::MembersMapper, services: true do end it 'maps a project member' do - expect(project_member_map_id(user2.id)).to eq(user2.id) + expect(project_member_user_id(user2.id)).to eq(user2.id) end it 'defaults to importer project member if it does not exist' do - expect(project_member_map_id(-1)).to eq(user.id) + expect(project_member_user_id(-1)).to eq(user.id) end end - def project_member_map_id(id) - members_mapper.map[id]['id'] + def project_member_user_id(id) + members_mapper.map[id].user.id end end -- cgit v1.2.1 From 66d3f98582e2a9fbdff93402de1075b4f7f01eb3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 17:06:12 +0100 Subject: add id to project member user json --- app/services/projects/import_export.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index e281925fe9c..b0c0891edb5 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -17,7 +17,7 @@ module Projects private def members - [{ project_members: { include: [user: { only: [:email, :username] }] } }] + [{ project_members: { include: [user: { only: [:id, :email, :username] }] } }] end def storage_path -- cgit v1.2.1 From 1d8e02c4ceba34addae175ac7797535eac2c7457 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 17:33:45 +0100 Subject: solved a bunch of issues with mapping members/users --- app/services/projects/import_export/members_mapper.rb | 5 +++-- app/services/projects/import_export/relation_factory.rb | 13 ++++++++++++- spec/services/projects/import_export/members_mapper_spec.rb | 8 +++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/services/projects/import_export/members_mapper.rb b/app/services/projects/import_export/members_mapper.rb index 634683da5ce..8d47422b10c 100644 --- a/app/services/projects/import_export/members_mapper.rb +++ b/app/services/projects/import_export/members_mapper.rb @@ -28,9 +28,10 @@ module Projects end def assign_member(existing_user, member) + old_user_id = member['user']['id'] member['user'] = existing_user project_member = ProjectMember.new(member_hash(member)) - @project_member_map[existing_user.id] = project_member if project_member.save + @project_member_map[old_user_id] = project_member.user.id if project_member.save end def member_hash(member) @@ -42,7 +43,7 @@ module Projects @default_project_member ||= begin default_member = ProjectMember.new(default_project_member_hash) - default_member if default_member.save + default_member.user.id if default_member.save end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 624453a6ade..39ac29e3f80 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -3,17 +3,28 @@ module Projects module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets } + OVERRIDES = { snippets: :project_snippets }.freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze def create(relation_sym:, relation_hash:, members_map:) relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') #screw IDs for now + update_user_references(relation_hash, members_map) klass.new(relation_hash) end private + #TODO nice to have, optimize this to only get called for specific models + def update_user_references(relation_hash, members_map) + USER_REFERENCES.each do |reference| + if relation_hash[reference] + relation_hash[reference] = members_map[relation_hash[reference]] + end + end + end + def relation_class(relation_sym) relation_sym.to_s.classify.constantize end diff --git a/spec/services/projects/import_export/members_mapper_spec.rb b/spec/services/projects/import_export/members_mapper_spec.rb index dc6ccb50121..e222dd42053 100644 --- a/spec/services/projects/import_export/members_mapper_spec.rb +++ b/spec/services/projects/import_export/members_mapper_spec.rb @@ -6,6 +6,7 @@ describe Projects::ImportExport::MembersMapper, services: true do let(:user) { create(:user) } let(:project) { create(:project, :public, name: 'searchable_project') } let(:user2) { create(:user) } + let(:exported_user_id) { 99 } let(:exported_members) do [{ "id" => 2, @@ -22,6 +23,7 @@ describe Projects::ImportExport::MembersMapper, services: true do "invite_accepted_at" => nil, "user" => { + "id" => exported_user_id, "email" => user2.email, "username" => user2.username } @@ -34,15 +36,15 @@ describe Projects::ImportExport::MembersMapper, services: true do end it 'maps a project member' do - expect(project_member_user_id(user2.id)).to eq(user2.id) + expect(members_mapper.map[exported_user_id]).to eq(user2.id) end it 'defaults to importer project member if it does not exist' do - expect(project_member_user_id(-1)).to eq(user.id) + expect(members_mapper.map[-1]).to eq(user.id) end end def project_member_user_id(id) - members_mapper.map[id].user.id + members_mapper.map[id] end end -- cgit v1.2.1 From 76efdea78839af27127cc81358ea1ffd911191f4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 11 Mar 2016 18:17:57 +0100 Subject: fixing MR issues with import --- app/models/merge_request.rb | 4 +++- app/services/projects/import_export/project_tree_restorer.rb | 2 +- app/services/projects/import_export/relation_factory.rb | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8271dc83116..714d55323d4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -50,6 +50,8 @@ class MergeRequest < ActiveRecord::Base delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil + attr_accessor :importing + # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests attr_accessor :allow_broken @@ -128,7 +130,7 @@ class MergeRequest < ActiveRecord::Base validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches + validate :validate_branches, unless: :importing validate :validate_fork scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) } diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 87b78197cd6..ac9b7132004 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -44,7 +44,7 @@ module Projects def create_relation(relation, relation_hash_list) relation_hash_list.map do |relation_hash| Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash.merge(project_id: project.id), members_map: members_map) + relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) end end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 39ac29e3f80..1b9bd234a43 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -10,6 +10,12 @@ module Projects relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') #screw IDs for now + #TODO refactor this... + if relation_sym == :merge_requests + relation_hash['target_project_id'] = relation_hash.delete('project_id') + relation_hash['source_project_id'] = -1 + relation_hash['importing'] = true + end update_user_references(relation_hash, members_map) klass.new(relation_hash) end -- cgit v1.2.1 From 3a131383b68dcb14abd9d31a2a462ffa57841095 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 14 Mar 2016 10:35:41 +0100 Subject: fixed MR issue and refactored some stuff --- app/models/merge_request.rb | 2 +- app/services/projects/import_export/members_mapper.rb | 8 ++------ app/services/projects/import_export/relation_factory.rb | 13 +++++++------ 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 714d55323d4..de6189b2dc8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -125,7 +125,7 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: :allow_broken + validates :source_project, presence: true, unless: [:allow_broken, :importing] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true diff --git a/app/services/projects/import_export/members_mapper.rb b/app/services/projects/import_export/members_mapper.rb index 8d47422b10c..6d49d901bf3 100644 --- a/app/services/projects/import_export/members_mapper.rb +++ b/app/services/projects/import_export/members_mapper.rb @@ -13,12 +13,6 @@ module Projects end def map - @project_member_map ||= project_member_map - end - - private - - def project_member_map @project_member_map = Hash.new(default_project_member) @exported_members.each do |member| existing_user = User.where(find_project_user_query(member)).first @@ -27,6 +21,8 @@ module Projects @project_member_map end + private + def assign_member(existing_user, member) old_user_id = member['user']['id'] member['user'] = existing_user diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 1b9bd234a43..6ccaa409ce9 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -10,18 +10,19 @@ module Projects relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') #screw IDs for now - #TODO refactor this... - if relation_sym == :merge_requests - relation_hash['target_project_id'] = relation_hash.delete('project_id') - relation_hash['source_project_id'] = -1 - relation_hash['importing'] = true - end + handle_merge_requests(relation_hash) if relation_sym == :merge_requests update_user_references(relation_hash, members_map) klass.new(relation_hash) end private + def handle_merge_requests(relation_hash) + relation_hash['target_project_id'] = relation_hash.delete('project_id') + relation_hash['source_project_id'] = -1 + relation_hash['importing'] = true + end + #TODO nice to have, optimize this to only get called for specific models def update_user_references(relation_hash, members_map) USER_REFERENCES.each do |reference| -- cgit v1.2.1 From 37c68fe025b3b4dd4ac3b510cbb32d42cbe6b23b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 14 Mar 2016 11:12:00 +0100 Subject: don't create MR diff as this can be imported presumably --- app/models/merge_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index de6189b2dc8..d9fbbb85003 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -45,7 +45,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash - after_create :create_merge_request_diff + after_create :create_merge_request_diff, unless: :importing after_update :update_merge_request_diff delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil -- cgit v1.2.1 From ba1fcf36197b86049b5e6a0f2da8842b236bcf62 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 14 Mar 2016 12:29:46 +0100 Subject: added commits and merge request diffs to export --- app/services/projects/import_export.rb | 12 ++++++++++-- .../projects/import_export/project_tree_saver_spec.rb | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index b0c0891edb5..373916a0c4c 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -11,13 +11,21 @@ module Projects end def project_tree - %i(issues merge_requests labels milestones snippets releases events commit_statuses) + members + %i(issues labels milestones snippets releases events) + [members, merge_requests, commit_statuses] end private + def merge_requests + { merge_requests: { include: :merge_request_diff } } + end + + def commit_statuses + { commit_statuses: { include: :commit } } + end + def members - [{ project_members: { include: [user: { only: [:id, :email, :username] }] } }] + { project_members: { include: [user: { only: [:id, :email, :username] }] } } end def storage_path diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 383b3e4ce8b..960109e0c84 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -85,6 +85,14 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do it 'has project members' do expect(saved_project_json['project_members']).not_to be_empty end + + it 'has merge requests diffs' do + expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty + end + + it 'has ci commits' do + expect(saved_project_json['commit_statuses'].first['commit']).not_to be_empty + end end end -- cgit v1.2.1 From e05bc6114491f4c1f84505b4c4746faa8c269420 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 14 Mar 2016 12:58:01 +0100 Subject: fix project snippets json issue --- app/services/projects/import_export.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index 373916a0c4c..e1d7458a13b 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -11,11 +11,15 @@ module Projects end def project_tree - %i(issues labels milestones snippets releases events) + [members, merge_requests, commit_statuses] + %i(issues labels milestones releases events) + [snippets, members, merge_requests, commit_statuses] end private + def snippets + { snippets: { except: :expired_at } } + end + def merge_requests { merge_requests: { include: :merge_request_diff } } end -- cgit v1.2.1 From a59ba217ae6796d4589575e97913aa80add75b43 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 14 Mar 2016 18:32:56 +0100 Subject: WIP - building up import tree with sub relations, etc --- app/services/projects/import_export.rb | 4 +++ .../import_export/project_tree_restorer.rb | 35 ++++++++++++++++++---- fixtures/import_export/project.json | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index 373916a0c4c..51a1732a675 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -10,6 +10,10 @@ module Projects %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) end + def project_tree_list + project_tree.map {|r| r.is_a?(Hash) ? r.keys.first : r } + end + def project_tree %i(issues labels milestones snippets releases events) + [members, merge_requests, commit_statuses] end diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index ac9b7132004..ae1d0506c0d 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -12,6 +12,7 @@ module Projects json = IO.read(@path) @tree_hash = ActiveSupport::JSON.decode(json) create_relations + puts project.inspect end private @@ -21,11 +22,33 @@ module Projects exported_members: @tree_hash.delete('project_members'), user: @user, project_id: project.id) end - def create_relations - (ImportExport.project_tree - [:project_members]).each do |relation| - next if @tree_hash[relation.to_s].empty? - relation_hash = create_relation(relation, @tree_hash[relation.to_s]) - project.update_attribute(relation, relation_hash) + #TODO Definitely refactor this method! + def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) + relation_list.each do |relation| + relation_hash = nil + # FIXME + # next if tree_hash[relation.to_s].blank? + if (relation.is_a?(Hash) && relation.values.first[:include]) + #TODO name stuff properly + relation_sym = relation.keys.first + #TODO remove sub-relation hashes from here so we can save the parent relation first + relation_hash = create_relation(relation_sym, tree_hash[relation_sym.to_s]) + sub_relations = [] + sub_relation = relation.values.first[:include] + sub_relation_hash_list = tree_hash[relation.keys.first.to_s] + sub_relation_hash_list.each do |sub_relation_hash| + sub_relations << create_relation(relation, sub_relation_hash[relation.to_s]) + end + relation_hash.update_attribute(sub_relation, sub_relations) + end + relation_hash ||= create_relation(relation, tree_hash[relation.to_s]) + project.update_attribute(relation, relation_hash) + end + end + + def default_relation_list + ImportExport.project_tree.reject do |rel| + rel.is_a?(Hash) && !rel[:project_members].blank? end end @@ -42,7 +65,7 @@ module Projects end def create_relation(relation, relation_hash_list) - relation_hash_list.map do |relation_hash| + [relation_hash_list].flatten.map do |relation_hash| Projects::ImportExport::RelationFactory.create( relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) end diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json index 38c999e8fb1..d21e754ce3d 100644 --- a/fixtures/import_export/project.json +++ b/fixtures/import_export/project.json @@ -1 +1 @@ -{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Voluptatem quia repudiandae id rem ducimus.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-11T10:21:37.525Z","updated_at":"2016-03-11T10:21:44.516Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Enim dolor veritatis suscipit sed laboriosam consequatur doloremque.","created_at":"2016-03-11T10:21:38.628Z","updated_at":"2016-03-11T10:21:38.628Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-11T10:21:39.964Z","updated_at":"2016-03-11T10:21:44.539Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-11T10:21:44.688Z","updated_at":"2016-03-11T10:21:44.688Z","state":"active","iid":1}],"snippets":[{"id":2,"title":"Dolorem perspiciatis unde aut similique aperiam totam qui iste.","content":"Sed similique praesentium tempora et ad dolor quis ut.","author_id":30,"project_id":14,"created_at":"2016-03-11T10:21:41.034Z","updated_at":"2016-03-11T10:21:44.557Z","file_name":"dee","expires_at":null,"visibility_level":0}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-11T10:21:42.531Z","updated_at":"2016-03-11T10:21:44.576Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-11T10:21:44.892Z","updated_at":"2016-03-11T10:21:44.892Z","action":8,"author_id":19}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-11T10:21:43.493Z","updated_at":"2016-03-11T10:21:44.620Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null}],"project_members":[{"id":2,"access_level":40,"source_id":14,"source_type":"Project","user_id":19,"notification_level":3,"created_at":"2016-03-11T10:21:44.822Z","updated_at":"2016-03-11T10:21:44.822Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"user":{"email":"claudie.lemke@prohaskamarquardt.co.uk","username":"hiram_conroy19"}}]} \ No newline at end of file +{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Libero explicabo dolores atque quae debitis sit ipsam unde.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-14T11:56:57.324Z","updated_at":"2016-03-14T11:57:02.118Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-14T11:56:59.538Z","updated_at":"2016-03-14T11:57:02.132Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-14T11:57:02.241Z","updated_at":"2016-03-14T11:57:02.241Z","state":"active","iid":1}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-14T11:57:00.950Z","updated_at":"2016-03-14T11:57:02.159Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-14T11:57:02.392Z","updated_at":"2016-03-14T11:57:02.392Z","action":8,"author_id":19}],"snippets":[{"id":2,"title":"Et qui optio blanditiis non.","content":"Repudiandae hic id vero adipisci.","author_id":28,"project_id":14,"created_at":"2016-03-14T11:57:00.372Z","updated_at":"2016-03-14T11:57:02.144Z","file_name":"krystina","visibility_level":0}],"project_members":[{"id":2,"access_level":40,"source_id":14,"source_type":"Project","user_id":19,"notification_level":3,"created_at":"2016-03-14T11:57:02.335Z","updated_at":"2016-03-14T11:57:02.335Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"user":{"id":19,"email":"braxton@greenholttromp.ca","username":"elena19"}}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Cum est in est et.","created_at":"2016-03-14T11:56:58.333Z","updated_at":"2016-03-14T11:56:58.333Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"merge_request_diff":{"id":2,"state":"collected","st_commits":[{"id":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["570e7b2abdd848b95f2f578043fc23bd6f6fd24d"],"authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"],"authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["d14d6c0abdd253381df51a723d58691b2ee1ab08"],"authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["c1acaa58bbcbc3eafe538cb8274ba387047b69f8"],"authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["ae73cb07c9eeaf35924a10f713b364d32b2dd34f"],"authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"}],"st_diffs":[{"diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true},{"diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true},{"diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false},{"diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false}],"merge_request_id":2,"created_at":"2016-03-14T11:56:58.353Z","updated_at":"2016-03-14T11:56:58.635Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"8"}}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-14T11:57:01.483Z","updated_at":"2016-03-14T11:57:02.190Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null,"commit":{"id":2,"project_id":13,"ref":null,"sha":"97de212e80737a608d939f648d959671fb0a0142","before_sha":null,"push_data":null,"created_at":"2016-03-14T11:57:01.468Z","updated_at":"2016-03-14T11:57:01.468Z","tag":false,"yaml_errors":null,"committed_at":null,"gl_project_id":13}}]} \ No newline at end of file -- cgit v1.2.1 From e17c7f41fc9fc95399896622eac053fb276888df Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 15 Mar 2016 09:17:44 +0100 Subject: add TODO --- app/services/projects/import_export/project_tree_restorer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index ae1d0506c0d..3a767c1980e 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -23,6 +23,7 @@ module Projects end #TODO Definitely refactor this method! + #TODO Think about having a yaml file to describe the tree instead of just hashes? def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) relation_list.each do |relation| relation_hash = nil -- cgit v1.2.1 From efde96e9cd4b0096c19ecfa46237b7eda28038aa Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 6 Apr 2016 18:12:41 +0200 Subject: started refactored the dynamic rules to be defined in a yml file --- .../projects/import_export/import_export.yml | 31 ++++++++++++++ .../projects/import_export/import_export_reader.rb | 47 ++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 app/services/projects/import_export/import_export.yml create mode 100644 app/services/projects/import_export/import_export_reader.rb diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml new file mode 100644 index 00000000000..d02ec5df2ff --- /dev/null +++ b/app/services/projects/import_export/import_export.yml @@ -0,0 +1,31 @@ +# Class relationships to be included in the import/export +:project_tree: + - :issues + - :labels + - :milestones + - :snippets + - :releases + - :events + - :project_members: + - :user + - :merge_requests: + - :merge_request_diff + - :commit_statuses: + - :commit + +:attributes_only: + :project: + - :name + - :path + - :description + - :issues_enabled + - :wall_enabled + - :merge_requests_enabled + - :wiki_enabled + - :snippets_enabled + - :visibility_level + - :archived + :user: + - :id + - :email + - :username \ No newline at end of file diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb new file mode 100644 index 00000000000..af8c8c3c251 --- /dev/null +++ b/app/services/projects/import_export/import_export_reader.rb @@ -0,0 +1,47 @@ +module Projects + module ImportExport + module ImportExportReader + extend self + + def project_tree + { only: atts_only[:project], include: build_hash(tree) } + end + + def config + @config ||= YAML.load_file('app/services/projects/import_export/import_export.yml') + end + + def atts_only + config[:attributes_only] + end + + def atts_except + config[:attributes_except] + end + + def tree + config[:project_tree] + end + + def build_hash(array) + array.map { |el| el.is_a?(Hash) ? process_include(el) : el } + end + + def process_include(hash) + included_classes_hash = {} + hash.values.flatten.each do |value| + value = value.is_a?(Hash) ? process_include(hash) : value + new_hash = { :include => value } + new_hash.merge!(check_only(value)) + included_classes_hash[hash.keys.first] = new_hash + end + included_classes_hash + end + + def check_only(value) + key = value.is_a?(Hash) ? value.keys.first : value + atts_only[key].nil? ? {} : { only: atts_only[key] } + end + end + end +end \ No newline at end of file -- cgit v1.2.1 From acfa0b69d5370749cee9768bf79b58daf6d916a4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 7 Apr 2016 13:19:57 +0200 Subject: continuing to refactor config, added spec and fixed a few problems --- .../projects/import_export/import_export.yml | 2 +- .../projects/import_export/import_export_reader.rb | 51 +++++++++++++++++++--- .../import_export/import_export_reader_spec.rb | 24 ++++++++++ spec/support/import_export/import_export.yml | 20 +++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 spec/services/projects/import_export/import_export_reader_spec.rb create mode 100644 spec/support/import_export/import_export.yml diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml index d02ec5df2ff..40fd98b8092 100644 --- a/app/services/projects/import_export/import_export.yml +++ b/app/services/projects/import_export/import_export.yml @@ -1,4 +1,4 @@ -# Class relationships to be included in the import/export +# Class relationships to be included in the project import/export :project_tree: - :issues - :labels diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb index af8c8c3c251..77a1abb851b 100644 --- a/app/services/projects/import_export/import_export_reader.rb +++ b/app/services/projects/import_export/import_export_reader.rb @@ -7,6 +7,8 @@ module Projects { only: atts_only[:project], include: build_hash(tree) } end + private + def config @config ||= YAML.load_file('app/services/projects/import_export/import_export.yml') end @@ -27,21 +29,56 @@ module Projects array.map { |el| el.is_a?(Hash) ? process_include(el) : el } end - def process_include(hash) - included_classes_hash = {} + def process_include(hash, included_classes_hash = {}) hash.values.flatten.each do |value| - value = value.is_a?(Hash) ? process_include(hash) : value - new_hash = { :include => value } - new_hash.merge!(check_only(value)) - included_classes_hash[hash.keys.first] = new_hash + current_key, value = process_current_class(hash, included_classes_hash, value) + if included_classes_hash[current_key] + add_class(current_key, included_classes_hash, value) + else + add_new_class(current_key, included_classes_hash, value) + end end included_classes_hash end + def process_current_class(hash, included_classes_hash, value) + value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value + current_key = hash.keys.first + current_key_only = check_only_and_except(current_key) + included_classes_hash[current_key] ||= current_key_only unless current_key_only.empty? + return current_key, value + end + + def add_new_class(current_key, included_classes_hash, value) + new_hash = { :include => value } + new_hash.merge!(check_only_and_except(value)) + included_classes_hash[current_key] = new_hash + end + + def add_class(current_key, included_classes_hash, value) + check_only_hash = check_only_and_except(value) + value = { value => check_only_hash } unless check_only_hash.empty? + old_values = included_classes_hash[current_key][:include] + included_classes_hash[current_key][:include] = ([old_values] + [value]).compact.flatten + end + + def check_only_and_except(value) + check_only(value).merge(check_except(value)) + end + def check_only(value) - key = value.is_a?(Hash) ? value.keys.first : value + key = key_from_hash(value) atts_only[key].nil? ? {} : { only: atts_only[key] } end + + def check_except(value) + key = key_from_hash(value) + atts_except[key].nil? ? {} : { except: atts_except[key] } + end + + def key_from_hash(value) + value.is_a?(Hash) ? value.keys.first : value + end end end end \ No newline at end of file diff --git a/spec/services/projects/import_export/import_export_reader_spec.rb b/spec/services/projects/import_export/import_export_reader_spec.rb new file mode 100644 index 00000000000..73e043e52ce --- /dev/null +++ b/spec/services/projects/import_export/import_export_reader_spec.rb @@ -0,0 +1,24 @@ +require 'rspec' + +describe Projects::ImportExport::ImportExportReader do + + let(:test_config) { 'spec/support/import_export/import_export.yml' } + let(:project_tree_hash) do + { + :only => [:name, :path], + :include => [:issues, :labels, + { :merge_requests => { + :only => [:id], + :except => [:iid], + :include => [:merge_request_diff, :merge_request_test] + } }, + { :commit_statuses => { :include => :commit } }] + } + end + + it 'should generate hash from project tree config' do + allow(described_class).to receive(:config).and_return(YAML.load_file(test_config)) + + expect(described_class.project_tree).to eq(project_tree_hash) + end +end \ No newline at end of file diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml new file mode 100644 index 00000000000..e6985d6413c --- /dev/null +++ b/spec/support/import_export/import_export.yml @@ -0,0 +1,20 @@ +# Class relationships to be included in the project import/export +:project_tree: + - :issues + - :labels + - :merge_requests: + - :merge_request_diff + - :merge_request_test + - :commit_statuses: + - :commit + +:attributes_only: + :project: + - :name + - :path + :merge_requests: + - :id + +:attributes_except: + :merge_requests: + - :iid \ No newline at end of file -- cgit v1.2.1 From de6c44e983a788262b72355b85d085a675c9fccc Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 7 Apr 2016 18:14:42 +0200 Subject: using new config --- app/services/projects/import_export.rb | 18 +----------------- app/services/projects/import_export/import_export.yml | 6 +++++- .../projects/import_export/import_export_reader.rb | 17 ++++++++++++----- .../projects/import_export/project_tree_saver.rb | 2 +- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb index e1d7458a13b..c74f1d31490 100644 --- a/app/services/projects/import_export.rb +++ b/app/services/projects/import_export.rb @@ -11,27 +11,11 @@ module Projects end def project_tree - %i(issues labels milestones releases events) + [snippets, members, merge_requests, commit_statuses] + Projects::ImportExport::ImportExportReader.project_tree end private - def snippets - { snippets: { except: :expired_at } } - end - - def merge_requests - { merge_requests: { include: :merge_request_diff } } - end - - def commit_statuses - { commit_statuses: { include: :commit } } - end - - def members - { project_members: { include: [user: { only: [:id, :email, :username] }] } } - end - def storage_path File.join(Settings.shared['path'], 'tmp/project_exports') end diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml index 40fd98b8092..be7680775ba 100644 --- a/app/services/projects/import_export/import_export.yml +++ b/app/services/projects/import_export/import_export.yml @@ -28,4 +28,8 @@ :user: - :id - :email - - :username \ No newline at end of file + - :username + +:attributes_except: + :snippets: + - :expired_at \ No newline at end of file diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb index 77a1abb851b..981f19c0acb 100644 --- a/app/services/projects/import_export/import_export_reader.rb +++ b/app/services/projects/import_export/import_export_reader.rb @@ -26,7 +26,14 @@ module Projects end def build_hash(array) - array.map { |el| el.is_a?(Hash) ? process_include(el) : el } + array.map do |el| + if el.is_a?(Hash) + process_include(el) + else + only_except_hash = check_only_and_except(el) + only_except_hash.empty? ? el : { el => only_except_hash } + end + end end def process_include(hash, included_classes_hash = {}) @@ -44,8 +51,8 @@ module Projects def process_current_class(hash, included_classes_hash, value) value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value current_key = hash.keys.first - current_key_only = check_only_and_except(current_key) - included_classes_hash[current_key] ||= current_key_only unless current_key_only.empty? + only_except_hash = check_only_and_except(current_key) + included_classes_hash[current_key] ||= only_except_hash unless only_except_hash.empty? return current_key, value end @@ -56,8 +63,8 @@ module Projects end def add_class(current_key, included_classes_hash, value) - check_only_hash = check_only_and_except(value) - value = { value => check_only_hash } unless check_only_hash.empty? + only_except_hash = check_only_and_except(value) + value = { value => only_except_hash } unless only_except_hash.empty? old_values = included_classes_hash[current_key][:include] included_classes_hash[current_key][:include] = ([old_values] + [value]).compact.flatten end diff --git a/app/services/projects/import_export/project_tree_saver.rb b/app/services/projects/import_export/project_tree_saver.rb index 3ca90d1f79a..44d100acd1d 100644 --- a/app/services/projects/import_export/project_tree_saver.rb +++ b/app/services/projects/import_export/project_tree_saver.rb @@ -32,7 +32,7 @@ module Projects def project_json_tree # TODO confirm children, also add subchildren (i.e comments) # TODO confirm atts for children - @project.to_json(only: ImportExport.project_atts, include: ImportExport.project_tree) + @project.to_json(Projects::ImportExport.project_tree) end end end -- cgit v1.2.1 From 5d642fb7aa666063d5e1761833463bc95241889a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 8 Apr 2016 15:07:12 +0200 Subject: fix rubocop warnings --- .../projects/import_export/import_export_reader.rb | 14 +++++++------- .../import_export/import_export_reader_spec.rb | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb index 981f19c0acb..e0617ce851c 100644 --- a/app/services/projects/import_export/import_export_reader.rb +++ b/app/services/projects/import_export/import_export_reader.rb @@ -38,7 +38,8 @@ module Projects def process_include(hash, included_classes_hash = {}) hash.values.flatten.each do |value| - current_key, value = process_current_class(hash, included_classes_hash, value) + current_key = hash.keys.first + value = process_current_class(hash, included_classes_hash, value) if included_classes_hash[current_key] add_class(current_key, included_classes_hash, value) else @@ -50,14 +51,13 @@ module Projects def process_current_class(hash, included_classes_hash, value) value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value - current_key = hash.keys.first - only_except_hash = check_only_and_except(current_key) - included_classes_hash[current_key] ||= only_except_hash unless only_except_hash.empty? - return current_key, value + only_except_hash = check_only_and_except(hash.keys.first) + included_classes_hash[hash.keys.first] ||= only_except_hash unless only_except_hash.empty? + value end def add_new_class(current_key, included_classes_hash, value) - new_hash = { :include => value } + new_hash = { include: value } new_hash.merge!(check_only_and_except(value)) included_classes_hash[current_key] = new_hash end @@ -88,4 +88,4 @@ module Projects end end end -end \ No newline at end of file +end diff --git a/spec/services/projects/import_export/import_export_reader_spec.rb b/spec/services/projects/import_export/import_export_reader_spec.rb index 73e043e52ce..f825292ce7e 100644 --- a/spec/services/projects/import_export/import_export_reader_spec.rb +++ b/spec/services/projects/import_export/import_export_reader_spec.rb @@ -5,14 +5,14 @@ describe Projects::ImportExport::ImportExportReader do let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do { - :only => [:name, :path], - :include => [:issues, :labels, - { :merge_requests => { - :only => [:id], - :except => [:iid], - :include => [:merge_request_diff, :merge_request_test] - } }, - { :commit_statuses => { :include => :commit } }] + only: [:name, :path], + include: [:issues, :labels, + { merge_requests: { + only: [:id], + except: [:iid], + include: [:merge_request_diff, :merge_request_test] + } }, + { commit_statuses: { include: :commit } }] } end @@ -21,4 +21,4 @@ describe Projects::ImportExport::ImportExportReader do expect(described_class.project_tree).to eq(project_tree_hash) end -end \ No newline at end of file +end -- cgit v1.2.1 From a28be02e88f1d213649ef2c25858313caeb83b59 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 8 Apr 2016 18:13:19 +0200 Subject: starting to use the new dynamic stuff on the import --- .../projects/import_export/import_export_reader.rb | 8 ++-- .../import_export/project_tree_restorer.rb | 47 +++++++++++++--------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb index e0617ce851c..001d24af604 100644 --- a/app/services/projects/import_export/import_export_reader.rb +++ b/app/services/projects/import_export/import_export_reader.rb @@ -7,6 +7,10 @@ module Projects { only: atts_only[:project], include: build_hash(tree) } end + def tree + config[:project_tree] + end + private def config @@ -21,10 +25,6 @@ module Projects config[:attributes_except] end - def tree - config[:project_tree] - end - def build_hash(array) array.map do |el| if el.is_a?(Hash) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 3a767c1980e..ca68840e988 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -25,32 +25,41 @@ module Projects #TODO Definitely refactor this method! #TODO Think about having a yaml file to describe the tree instead of just hashes? def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) + members_map # TODO remove this and fix project_members relation_list.each do |relation| - relation_hash = nil - # FIXME - # next if tree_hash[relation.to_s].blank? - if (relation.is_a?(Hash) && relation.values.first[:include]) - #TODO name stuff properly - relation_sym = relation.keys.first - #TODO remove sub-relation hashes from here so we can save the parent relation first - relation_hash = create_relation(relation_sym, tree_hash[relation_sym.to_s]) - sub_relations = [] - sub_relation = relation.values.first[:include] - sub_relation_hash_list = tree_hash[relation.keys.first.to_s] - sub_relation_hash_list.each do |sub_relation_hash| - sub_relations << create_relation(relation, sub_relation_hash[relation.to_s]) + if relation.is_a?(Hash) + relation.values.each do |value| + create_relations(value, @tree_hash[relation.to_s]) end - relation_hash.update_attribute(sub_relation, sub_relations) end - relation_hash ||= create_relation(relation, tree_hash[relation.to_s]) - project.update_attribute(relation, relation_hash) + relation_hash = create_relation(relation, tree_hash[relation.to_s]) + project.update_attribute(relation, relation_hash) + # relation_hash = nil + # # FIXME + # # next if tree_hash[relation.to_s].blank? + # if (relation.is_a?(Hash) && relation.values.first[:include]) + # #TODO name stuff properly + # relation_sym = relation.keys.first + # #TODO remove sub-relation hashes from here so we can save the parent relation first + # relation_hash = create_relation(relation_sym, tree_hash[relation_sym.to_s]) + # sub_relations = [] + # sub_relation = relation.values.first[:include] + # sub_relation_hash_list = tree_hash[relation.keys.first.to_s] + # sub_relation_hash_list.each do |sub_relation_hash| + # sub_relations << create_relation(relation, sub_relation_hash[relation.to_s]) + # end + # relation_hash.update_attribute(sub_relation, sub_relations) + # end + # relation_hash ||= create_relation(relation, tree_hash[relation.to_s]) + # project.update_attribute(relation, relation_hash) end end def default_relation_list - ImportExport.project_tree.reject do |rel| - rel.is_a?(Hash) && !rel[:project_members].blank? - end + Projects::ImportExport::ImportExportReader.tree + # ImportExport.project_tree.reject do |rel| + # rel.is_a?(Hash) && !rel[:project_members].blank? + # end end def project -- cgit v1.2.1 From 56fc58930cec501bd63046781c06be510feee029 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 11 Apr 2016 18:30:54 +0200 Subject: more refactoring to use dynamic import --- .../projects/import_export/import_export.yml | 4 ++- .../import_export/project_tree_restorer.rb | 35 +++++++++++++--------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml index be7680775ba..16a212d4c38 100644 --- a/app/services/projects/import_export/import_export.yml +++ b/app/services/projects/import_export/import_export.yml @@ -1,4 +1,4 @@ -# Class relationships to be included in the project import/export +# Model relationships to be included in the project import/export :project_tree: - :issues - :labels @@ -13,6 +13,7 @@ - :commit_statuses: - :commit +# Only include the following attributes for the models specified. :attributes_only: :project: - :name @@ -30,6 +31,7 @@ - :email - :username +# Do not include the following attributes for the models specified. :attributes_except: :snippets: - :expired_at \ No newline at end of file diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index ca68840e988..9512fcb2a1a 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -11,29 +11,25 @@ module Projects def restore json = IO.read(@path) @tree_hash = ActiveSupport::JSON.decode(json) + @project_members = @tree_hash.delete('project_members') create_relations - puts project.inspect end private def members_map @members ||= Projects::ImportExport::MembersMapper.map( - exported_members: @tree_hash.delete('project_members'), user: @user, project_id: project.id) + exported_members: @project_members, user: @user, project_id: project.id) end - #TODO Definitely refactor this method! - #TODO Think about having a yaml file to describe the tree instead of just hashes? def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) - members_map # TODO remove this and fix project_members relation_list.each do |relation| if relation.is_a?(Hash) - relation.values.each do |value| - create_relations(value, @tree_hash[relation.to_s]) - end + create_sub_relations(relation, tree_hash) end - relation_hash = create_relation(relation, tree_hash[relation.to_s]) - project.update_attribute(relation, relation_hash) + relation_key = relation.is_a?(Hash) ? relation.keys.first : relation + relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) + project.update_attribute(relation_key, relation_hash) # relation_hash = nil # # FIXME # # next if tree_hash[relation.to_s].blank? @@ -56,10 +52,7 @@ module Projects end def default_relation_list - Projects::ImportExport::ImportExportReader.tree - # ImportExport.project_tree.reject do |rel| - # rel.is_a?(Hash) && !rel[:project_members].blank? - # end + Projects::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } end def project @@ -74,6 +67,20 @@ module Projects project end + def create_sub_relations(relation, tree_hash) + # TODO refactor this + relation_key = relation.keys.first + tree_hash[relation_key.to_s].each do |relation_item| + relation.values.each do |sub_relation| + relation_hash = relation_item[sub_relation.to_s] + next if relation_hash.blank? + sub_relation_object = Projects::ImportExport::RelationFactory.create( + relation_sym: sub_relation, relation_hash: relation_hash, members_map: members_map) + relation_item[sub_relation.to_s] = sub_relation_object + end + end + end + def create_relation(relation, relation_hash_list) [relation_hash_list].flatten.map do |relation_hash| Projects::ImportExport::RelationFactory.create( -- cgit v1.2.1 From da60baf2a5fc5e2f7e84ea592b8ba6747a63abb3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 10:36:18 +0200 Subject: fixing more importing issues --- app/models/merge_request_diff.rb | 4 +++- app/services/projects/import_export/project_tree_restorer.rb | 3 ++- app/services/projects/import_export/relation_factory.rb | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 33884118595..03daac2822c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -37,7 +37,9 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_commits serialize :st_diffs - after_create :reload_content + after_create :reload_content, unless: :importing + + attr_accessor :importing def reload_content reload_commits diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 9512fcb2a1a..c498d7bd9bc 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -71,9 +71,10 @@ module Projects # TODO refactor this relation_key = relation.keys.first tree_hash[relation_key.to_s].each do |relation_item| - relation.values.each do |sub_relation| + relation.values.flatten.each do |sub_relation| relation_hash = relation_item[sub_relation.to_s] next if relation_hash.blank? + relation_hash.merge!('project_id' => project.id) if sub_relation == :merge_requests sub_relation_object = Projects::ImportExport::RelationFactory.create( relation_sym: sub_relation, relation_hash: relation_hash, members_map: members_map) relation_item[sub_relation.to_s] = sub_relation_object diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 6ccaa409ce9..4f1c753ac69 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -7,12 +7,14 @@ module Projects USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze def create(relation_sym:, relation_hash:, members_map:) + #TODO refactor this relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') #screw IDs for now handle_merge_requests(relation_hash) if relation_sym == :merge_requests update_user_references(relation_hash, members_map) - klass.new(relation_hash) + imported_object = klass.new(relation_hash) + imported_object.importing = true if imported_object.respond_to?(:importing) end private @@ -20,7 +22,6 @@ module Projects def handle_merge_requests(relation_hash) relation_hash['target_project_id'] = relation_hash.delete('project_id') relation_hash['source_project_id'] = -1 - relation_hash['importing'] = true end #TODO nice to have, optimize this to only get called for specific models -- cgit v1.2.1 From dbf755ae56f17d9b62a95a1f631ff583b6a50fb3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 11:34:58 +0200 Subject: small fix --- app/services/projects/import_export/relation_factory.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 4f1c753ac69..123dd43dad0 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -15,6 +15,7 @@ module Projects update_user_references(relation_hash, members_map) imported_object = klass.new(relation_hash) imported_object.importing = true if imported_object.respond_to?(:importing) + imported_object end private -- cgit v1.2.1 From 4ffcd427800b9d252b6b6a8cd9dafe03d203229a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 11:42:37 +0200 Subject: import working! --- app/services/projects/import_export/project_tree_restorer.rb | 3 +-- app/services/projects/import_export/relation_factory.rb | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index c498d7bd9bc..be0c7066099 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -74,9 +74,8 @@ module Projects relation.values.flatten.each do |sub_relation| relation_hash = relation_item[sub_relation.to_s] next if relation_hash.blank? - relation_hash.merge!('project_id' => project.id) if sub_relation == :merge_requests sub_relation_object = Projects::ImportExport::RelationFactory.create( - relation_sym: sub_relation, relation_hash: relation_hash, members_map: members_map) + relation_sym: sub_relation, relation_hash: relation_hash.merge!('project_id' => project.id), members_map: members_map) relation_item[sub_relation.to_s] = sub_relation_object end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 123dd43dad0..0eb7a03b6e9 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -3,7 +3,7 @@ module Projects module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets }.freeze + OVERRIDES = { snippets: :project_snippets, commit: 'Ci::Commit' }.freeze USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze def create(relation_sym:, relation_hash:, members_map:) @@ -11,6 +11,7 @@ module Projects relation_sym = parse_relation_sym(relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') #screw IDs for now + relation_hash.delete('project_id') unless klass.column_names.include?(:project_id) handle_merge_requests(relation_hash) if relation_sym == :merge_requests update_user_references(relation_hash, members_map) imported_object = klass.new(relation_hash) -- cgit v1.2.1 From eb55cb927b668e7e4e507ad87e65895c6a96f14d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 12:24:40 +0200 Subject: more refactoring --- .../import_export/project_tree_restorer.rb | 35 +++++++--------------- .../projects/import_export/relation_factory.rb | 24 ++++++++++----- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index be0c7066099..df0363e0eca 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -30,24 +30,8 @@ module Projects relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) project.update_attribute(relation_key, relation_hash) - # relation_hash = nil - # # FIXME - # # next if tree_hash[relation.to_s].blank? - # if (relation.is_a?(Hash) && relation.values.first[:include]) - # #TODO name stuff properly - # relation_sym = relation.keys.first - # #TODO remove sub-relation hashes from here so we can save the parent relation first - # relation_hash = create_relation(relation_sym, tree_hash[relation_sym.to_s]) - # sub_relations = [] - # sub_relation = relation.values.first[:include] - # sub_relation_hash_list = tree_hash[relation.keys.first.to_s] - # sub_relation_hash_list.each do |sub_relation_hash| - # sub_relations << create_relation(relation, sub_relation_hash[relation.to_s]) - # end - # relation_hash.update_attribute(sub_relation, sub_relations) - # end - # relation_hash ||= create_relation(relation, tree_hash[relation.to_s]) - # project.update_attribute(relation, relation_hash) + # FIXME + # next if tree_hash[relation.to_s].blank? end end @@ -68,14 +52,11 @@ module Projects end def create_sub_relations(relation, tree_hash) - # TODO refactor this - relation_key = relation.keys.first - tree_hash[relation_key.to_s].each do |relation_item| + tree_hash[relation.keys.first.to_s].each do |relation_item| relation.values.flatten.each do |sub_relation| relation_hash = relation_item[sub_relation.to_s] next if relation_hash.blank? - sub_relation_object = Projects::ImportExport::RelationFactory.create( - relation_sym: sub_relation, relation_hash: relation_hash.merge!('project_id' => project.id), members_map: members_map) + sub_relation_object = relation_from_factory(relation, relation_hash) relation_item[sub_relation.to_s] = sub_relation_object end end @@ -83,10 +64,14 @@ module Projects def create_relation(relation, relation_hash_list) [relation_hash_list].flatten.map do |relation_hash| - Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) + relation_from_factory(relation, relation_hash) end end + + def relation_from_factory(relation, relation_hash) + Projects::ImportExport::RelationFactory.create( + relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) + end end end end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 0eb7a03b6e9..31d81203645 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -7,16 +7,13 @@ module Projects USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze def create(relation_sym:, relation_hash:, members_map:) - #TODO refactor this relation_sym = parse_relation_sym(relation_sym) - klass = relation_class(relation_sym) - relation_hash.delete('id') #screw IDs for now - relation_hash.delete('project_id') unless klass.column_names.include?(:project_id) + klass = parse_relation(relation_hash, relation_sym) + handle_merge_requests(relation_hash) if relation_sym == :merge_requests update_user_references(relation_hash, members_map) - imported_object = klass.new(relation_hash) - imported_object.importing = true if imported_object.respond_to?(:importing) - imported_object + + imported_object(klass, relation_hash) end private @@ -42,6 +39,19 @@ module Projects def parse_relation_sym(relation_sym) OVERRIDES[relation_sym] || relation_sym end + + def imported_object(klass, relation_hash) + imported_object = klass.new(relation_hash) + imported_object.importing = true if imported_object.respond_to?(:importing) + imported_object + end + + def parse_relation(relation_hash, relation_sym) + klass = relation_class(relation_sym) + relation_hash.delete('id') #screw IDs for now + relation_hash.delete('project_id') unless klass.column_names.include?(:project_id) + klass + end end end end -- cgit v1.2.1 From 495397793e1530bb1ec453d1ae5ea9b77cd22c54 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 12:57:00 +0200 Subject: fix spec --- app/services/projects/import_export/project_tree_restorer.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index df0363e0eca..251221c2a6f 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -23,16 +23,18 @@ module Projects end def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) + saved = [] relation_list.each do |relation| if relation.is_a?(Hash) create_sub_relations(relation, tree_hash) end relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) - project.update_attribute(relation_key, relation_hash) + saved << project.update_attribute(relation_key, relation_hash) # FIXME # next if tree_hash[relation.to_s].blank? end + saved.all? end def default_relation_list @@ -56,7 +58,7 @@ module Projects relation.values.flatten.each do |sub_relation| relation_hash = relation_item[sub_relation.to_s] next if relation_hash.blank? - sub_relation_object = relation_from_factory(relation, relation_hash) + sub_relation_object = relation_from_factory(sub_relation, relation_hash) relation_item[sub_relation.to_s] = sub_relation_object end end -- cgit v1.2.1 From 3b99e4ae5bc9e4d1bf54c65bf633ad449361b9df Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 15:32:25 +0200 Subject: add issue and MR comments to default export. Fix spec --- .../projects/import_export/import_export.yml | 4 +- .../projects/import_export/import_export_reader.rb | 8 +- .../projects/import_export/project_tree_saver.rb | 2 - .../import_export/project_tree_saver_spec.rb | 171 ++------------------- 4 files changed, 19 insertions(+), 166 deletions(-) diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml index be7680775ba..c622447ef04 100644 --- a/app/services/projects/import_export/import_export.yml +++ b/app/services/projects/import_export/import_export.yml @@ -1,6 +1,7 @@ # Class relationships to be included in the project import/export :project_tree: - - :issues + - :issues: + - :notes - :labels - :milestones - :snippets @@ -10,6 +11,7 @@ - :user - :merge_requests: - :merge_request_diff + - :notes - :commit_statuses: - :commit diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb index e0617ce851c..001d24af604 100644 --- a/app/services/projects/import_export/import_export_reader.rb +++ b/app/services/projects/import_export/import_export_reader.rb @@ -7,6 +7,10 @@ module Projects { only: atts_only[:project], include: build_hash(tree) } end + def tree + config[:project_tree] + end + private def config @@ -21,10 +25,6 @@ module Projects config[:attributes_except] end - def tree - config[:project_tree] - end - def build_hash(array) array.map do |el| if el.is_a?(Hash) diff --git a/app/services/projects/import_export/project_tree_saver.rb b/app/services/projects/import_export/project_tree_saver.rb index 44d100acd1d..b2ca85bd7c6 100644 --- a/app/services/projects/import_export/project_tree_saver.rb +++ b/app/services/projects/import_export/project_tree_saver.rb @@ -30,8 +30,6 @@ module Projects end def project_json_tree - # TODO confirm children, also add subchildren (i.e comments) - # TODO confirm atts for children @project.to_json(Projects::ImportExport.project_tree) end end diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index 960109e0c84..eda9e08cdce 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Projects::ImportExport::ProjectTreeSaver, services: true do describe :save do + # TODO refactor this into a setup method + let(:user) { create(:user) } let(:issue) { create(:issue, assignee: user) } let(:merge_request) { create(:merge_request) } @@ -25,6 +27,8 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project, shared: shared) } + let!(:issue_note) { create(:note, note: ":+1: issue", noteable: issue) } + let!(:merge_request_note) { create(:note, note: ":+1: merge_request", noteable: merge_request) } before(:each) do project.team << [user, :master] @@ -78,6 +82,10 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues']).not_to be_empty end + it 'has issue comments' do + expect(saved_project_json['issues'].first['notes']).not_to be_empty + end + it 'has commit statuses' do expect(saved_project_json['commit_statuses']).not_to be_empty end @@ -90,6 +98,10 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty end + it 'has merge requests comments' do + expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty + end + it 'has ci commits' do expect(saved_project_json['commit_statuses'].first['commit']).not_to be_empty end @@ -99,163 +111,4 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do def project_json(filename) JSON.parse(IO.read(filename)) end - - # TODO: Remove this. Current JSON pretty printed: - # { - # "id":7, - # "name":"searchable_project", - # "path":"gitlabhq", - # "description":null, - # "issues_enabled":true, - # "wall_enabled":false, - # "merge_requests_enabled":true, - # "wiki_enabled":true, - # "snippets_enabled":true, - # "visibility_level":20, - # "archived":false, - # "issues":[ - # { - # "id":1, - # "title":"Voluptas dolores molestias iste excepturi quia atque sint et.", - # "assignee_id":1, - # "author_id":2, - # "project_id":7, - # "created_at":"2016-03-08T09:14:31.726Z", - # "updated_at":"2016-03-08T09:14:36.293Z", - # "position":0, - # "branch_name":null, - # "description":null, - # "milestone_id":null, - # "state":"opened", - # "iid":1, - # "updated_by_id":null - # } - # ], - # "merge_requests":[ - # { - # "id":1, - # "target_branch":"feature", - # "source_branch":"master", - # "source_project_id":2, - # "author_id":5, - # "assignee_id":null, - # "title":"Quam velit cupiditate culpa perspiciatis esse maiores quaerat.", - # "created_at":"2016-03-08T09:14:32.597Z", - # "updated_at":"2016-03-08T09:14:32.597Z", - # "milestone_id":null, - # "state":"opened", - # "merge_status":"can_be_merged", - # "target_project_id":7, - # "iid":1, - # "description":null, - # "position":0, - # "locked_at":null, - # "updated_by_id":null, - # "merge_error":null, - # "merge_params":{ - # - # }, - # "merge_when_build_succeeds":false, - # "merge_user_id":null, - # "merge_commit_sha":null - # } - # ], - # "labels":[ - # { - # "id":1, - # "title":"Bug", - # "color":"#990000", - # "project_id":7, - # "created_at":"2016-03-08T09:14:33.774Z", - # "updated_at":"2016-03-08T09:14:36.314Z", - # "template":false, - # "description":null - # } - # ], - # "milestones":[ - # { - # "id":1, - # "title":"Milestone v1.2", - # "project_id":7, - # "description":null, - # "due_date":null, - # "created_at":"2016-03-08T09:14:36.526Z", - # "updated_at":"2016-03-08T09:14:36.526Z", - # "state":"active", - # "iid":1 - # } - # ], - # "snippets":[ - # { - # "id":1, - # "title":"Voluptatem qui officiis modi ut fugit distinctio dolor qui.", - # "content":"Quaerat sunt eligendi voluptatum magnam.", - # "author_id":12, - # "project_id":7, - # "created_at":"2016-03-08T09:14:34.539Z", - # "updated_at":"2016-03-08T09:14:36.332Z", - # "file_name":"rowland.tremblay", - # "expires_at":null, - # "visibility_level":0 - # } - # ], - # "releases":[ - # { - # "id":1, - # "tag":"v1.1.0", - # "description":"Awesome release", - # "project_id":7, - # "created_at":"2016-03-08T09:14:35.023Z", - # "updated_at":"2016-03-08T09:14:36.351Z" - # } - # ], - # "events":[ - # { - # "id":1, - # "target_type":null, - # "target_id":null, - # "title":null, - # "data":null, - # "project_id":7, - # "created_at":"2016-03-08T09:14:36.806Z", - # "updated_at":"2016-03-08T09:14:36.806Z", - # "action":8, - # "author_id":1 - # } - # ], - # "commit_statuses":[ - # { - # "id":1, - # "project_id":null, - # "status":"success", - # "finished_at":"2016-01-26T07:23:42.000Z", - # "trace":null, - # "created_at":"2016-03-08T09:14:35.633Z", - # "updated_at":"2016-03-08T09:14:36.385Z", - # "started_at":"2016-01-26T07:21:42.000Z", - # "runner_id":null, - # "coverage":null, - # "commit_id":1, - # "commands":null, - # "job_id":null, - # "name":"default", - # "deploy":false, - # "options":null, - # "allow_failure":false, - # "stage":null, - # "trigger_request_id":null, - # "stage_idx":null, - # "tag":null, - # "ref":null, - # "user_id":null, - # "target_url":null, - # "description":"commit status", - # "artifacts_file":null, - # "gl_project_id":7, - # "artifacts_metadata":null, - # "erased_by_id":null, - # "erased_at":null - # } - # ] - # } end -- cgit v1.2.1 From 0d8a97dcef64967e3a8912e53ec8c91b54ccfe2e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 17:10:30 +0200 Subject: some more fixes to import projects --- .../import_export/project_tree_restorer.rb | 13 +- .../projects/import_export/relation_factory.rb | 4 +- fixtures/import_export/project.json | 433 ++++++++++++++++++++- 3 files changed, 445 insertions(+), 5 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 251221c2a6f..2d9dee1031a 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -58,12 +58,21 @@ module Projects relation.values.flatten.each do |sub_relation| relation_hash = relation_item[sub_relation.to_s] next if relation_hash.blank? - sub_relation_object = relation_from_factory(sub_relation, relation_hash) - relation_item[sub_relation.to_s] = sub_relation_object + process_sub_relation(relation_hash, relation_item, sub_relation) end end end + def process_sub_relation(relation_hash, relation_item, sub_relation) + sub_relation_object = nil + if relation_hash.is_a?(Array) + sub_relation_object = create_relation(sub_relation, relation_hash) + else + sub_relation_object = relation_from_factory(sub_relation, relation_hash) + end + relation_item[sub_relation.to_s] = sub_relation_object + end + def create_relation(relation, relation_hash_list) [relation_hash_list].flatten.map do |relation_hash| relation_from_factory(relation, relation_hash) diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 31d81203645..29917fce2ba 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -48,8 +48,8 @@ module Projects def parse_relation(relation_hash, relation_sym) klass = relation_class(relation_sym) - relation_hash.delete('id') #screw IDs for now - relation_hash.delete('project_id') unless klass.column_names.include?(:project_id) + relation_hash.delete('id') + relation_hash.delete('project_id') unless klass.column_names.include?('project_id') klass end end diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json index d21e754ce3d..a2fed10b85e 100644 --- a/fixtures/import_export/project.json +++ b/fixtures/import_export/project.json @@ -1 +1,432 @@ -{"name":"searchable_project","path":"gitlabhq","description":null,"issues_enabled":true,"wall_enabled":false,"merge_requests_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"visibility_level":20,"archived":false,"issues":[{"id":2,"title":"Libero explicabo dolores atque quae debitis sit ipsam unde.","assignee_id":19,"author_id":20,"project_id":14,"created_at":"2016-03-14T11:56:57.324Z","updated_at":"2016-03-14T11:57:02.118Z","position":0,"branch_name":null,"description":null,"milestone_id":null,"state":"opened","iid":1,"updated_by_id":null}],"labels":[{"id":2,"title":"Bug","color":"#990000","project_id":14,"created_at":"2016-03-14T11:56:59.538Z","updated_at":"2016-03-14T11:57:02.132Z","template":false,"description":null}],"milestones":[{"id":2,"title":"Milestone v1.2","project_id":14,"description":null,"due_date":null,"created_at":"2016-03-14T11:57:02.241Z","updated_at":"2016-03-14T11:57:02.241Z","state":"active","iid":1}],"releases":[{"id":2,"tag":"v1.1.0","description":"Awesome release","project_id":14,"created_at":"2016-03-14T11:57:00.950Z","updated_at":"2016-03-14T11:57:02.159Z"}],"events":[{"id":2,"target_type":null,"target_id":null,"title":null,"data":null,"project_id":14,"created_at":"2016-03-14T11:57:02.392Z","updated_at":"2016-03-14T11:57:02.392Z","action":8,"author_id":19}],"snippets":[{"id":2,"title":"Et qui optio blanditiis non.","content":"Repudiandae hic id vero adipisci.","author_id":28,"project_id":14,"created_at":"2016-03-14T11:57:00.372Z","updated_at":"2016-03-14T11:57:02.144Z","file_name":"krystina","visibility_level":0}],"project_members":[{"id":2,"access_level":40,"source_id":14,"source_type":"Project","user_id":19,"notification_level":3,"created_at":"2016-03-14T11:57:02.335Z","updated_at":"2016-03-14T11:57:02.335Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"user":{"id":19,"email":"braxton@greenholttromp.ca","username":"elena19"}}],"merge_requests":[{"id":2,"target_branch":"feature","source_branch":"master","source_project_id":9,"author_id":23,"assignee_id":null,"title":"Cum est in est et.","created_at":"2016-03-14T11:56:58.333Z","updated_at":"2016-03-14T11:56:58.333Z","milestone_id":null,"state":"opened","merge_status":"can_be_merged","target_project_id":14,"iid":1,"description":null,"position":0,"locked_at":null,"updated_by_id":null,"merge_error":null,"merge_params":{},"merge_when_build_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"merge_request_diff":{"id":2,"state":"collected","st_commits":[{"id":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["570e7b2abdd848b95f2f578043fc23bd6f6fd24d"],"authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"],"authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["d14d6c0abdd253381df51a723d58691b2ee1ab08"],"authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["c1acaa58bbcbc3eafe538cb8274ba387047b69f8"],"authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"id":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n","parent_ids":["ae73cb07c9eeaf35924a10f713b364d32b2dd34f"],"authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"}],"st_diffs":[{"diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true},{"diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true},{"diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false},{"diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false},{"diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false}],"merge_request_id":2,"created_at":"2016-03-14T11:56:58.353Z","updated_at":"2016-03-14T11:56:58.635Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"8"}}],"commit_statuses":[{"id":2,"project_id":null,"status":"success","finished_at":"2016-01-26T07:23:42.000Z","trace":null,"created_at":"2016-03-14T11:57:01.483Z","updated_at":"2016-03-14T11:57:02.190Z","started_at":"2016-01-26T07:21:42.000Z","runner_id":null,"coverage":null,"commit_id":2,"commands":null,"job_id":null,"name":"default","deploy":false,"options":null,"allow_failure":false,"stage":null,"trigger_request_id":null,"stage_idx":null,"tag":null,"ref":null,"user_id":null,"target_url":null,"description":"commit status","artifacts_file":null,"gl_project_id":14,"artifacts_metadata":null,"erased_by_id":null,"erased_at":null,"commit":{"id":2,"project_id":13,"ref":null,"sha":"97de212e80737a608d939f648d959671fb0a0142","before_sha":null,"push_data":null,"created_at":"2016-03-14T11:57:01.468Z","updated_at":"2016-03-14T11:57:01.468Z","tag":false,"yaml_errors":null,"committed_at":null,"gl_project_id":13}}]} \ No newline at end of file +{ + "name": "searchable_project", + "path": "gitlabhq", + "description": null, + "issues_enabled": true, + "wall_enabled": false, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "visibility_level": 20, + "archived": false, + "issues": [ + { + "id": 1, + "title": "Ad est est quia inventore eius suscipit molestiae.", + "assignee_id": 1, + "author_id": 2, + "project_id": 7, + "created_at": "2016-04-12T13:26:48.974Z", + "updated_at": "2016-04-12T13:26:55.496Z", + "position": 0, + "branch_name": null, + "description": null, + "milestone_id": null, + "state": "opened", + "iid": 1, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "notes": [ + { + "id": 1, + "note": ":+1: issue", + "noteable_type": "Issue", + "author_id": 21, + "created_at": "2016-04-12T13:26:55.480Z", + "updated_at": "2016-04-12T13:26:55.480Z", + "project_id": 8, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "labels": [ + { + "id": 1, + "title": "label1", + "color": "#990000", + "project_id": 7, + "created_at": "2016-04-12T13:26:51.027Z", + "updated_at": "2016-04-12T13:26:53.799Z", + "template": false, + "description": null + } + ], + "milestones": [ + { + "id": 1, + "title": "Milestone v1.2", + "project_id": 7, + "description": null, + "due_date": null, + "created_at": "2016-04-12T13:26:53.993Z", + "updated_at": "2016-04-12T13:26:53.993Z", + "state": "active", + "iid": 1 + } + ], + "snippets": [ + { + "id": 1, + "title": "Possimus harum est mollitia fugiat in.", + "content": "Itaque ipsum culpa quibusdam mollitia.", + "author_id": 10, + "project_id": 7, + "created_at": "2016-04-12T13:26:51.821Z", + "updated_at": "2016-04-12T13:26:53.811Z", + "file_name": "thomas_marquardt", + "visibility_level": 0 + } + ], + "releases": [ + { + "id": 1, + "tag": "v1.1.0", + "description": "Awesome release", + "project_id": 7, + "created_at": "2016-04-12T13:26:52.353Z", + "updated_at": "2016-04-12T13:26:53.823Z" + } + ], + "events": [ + { + "id": 1, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 7, + "created_at": "2016-04-12T13:26:57.139Z", + "updated_at": "2016-04-12T13:26:57.139Z", + "action": 8, + "author_id": 1 + } + ], + "project_members": [ + { + "id": 1, + "user": { + "id": 1, + "email": "maybell_auer@gleasonolson.com", + "created_at": "2016-04-12T13:26:47.499Z", + "updated_at": "2016-04-12T13:26:47.499Z", + "name": "Charles Nitzsche", + "admin": false, + "projects_limit": 42, + "skype": "", + "linkedin": "", + "twitter": "", + "authentication_token": "nCBdoTPWP-5adipsFwEq", + "theme_id": 2, + "bio": null, + "username": "koby.zieme1", + "can_create_group": true, + "can_create_team": false, + "state": "active", + "color_scheme_id": 1, + "notification_level": 1, + "password_expires_at": null, + "created_by_id": null, + "last_credential_check_at": null, + "avatar": { + "url": null + }, + "hide_no_ssh_key": false, + "website_url": "", + "notification_email": "maybell_auer@gleasonolson.com", + "hide_no_password": false, + "password_automatically_set": false, + "location": null, + "encrypted_otp_secret": null, + "encrypted_otp_secret_iv": null, + "encrypted_otp_secret_salt": null, + "otp_required_for_login": false, + "otp_backup_codes": null, + "public_email": "", + "dashboard": "projects", + "project_view": "readme", + "consumed_timestep": null, + "layout": "fixed", + "hide_project_limit": false, + "otp_grace_period_started_at": null, + "ldap_email": false, + "external": false + } + } + ], + "merge_requests": [ + { + "id": 1, + "target_branch": "feature", + "source_branch": "master", + "source_project_id": 2, + "author_id": 5, + "assignee_id": null, + "title": "Natus vel molestiae ab et dolorem odit ut.", + "created_at": "2016-04-12T13:26:49.813Z", + "updated_at": "2016-04-12T13:26:56.806Z", + "milestone_id": null, + "state": "opened", + "merge_status": "can_be_merged", + "target_project_id": 7, + "iid": 1, + "description": null, + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "merge_request_diff": { + "id": 1, + "state": "collected", + "st_commits": [ + { + "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + ], + "authored_date": "2014-02-27T10:01:38.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T10:01:38.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + ], + "authored_date": "2014-02-27T09:57:31.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:57:31.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "d14d6c0abdd253381df51a723d58691b2ee1ab08" + ], + "authored_date": "2014-02-27T09:54:21.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:54:21.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" + ], + "authored_date": "2014-02-27T09:49:50.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:49:50.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:48:32.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:48:32.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "new_path": ".DS_Store", + "old_path": ".DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "new_path": ".gitignore", + "old_path": ".gitignore", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "new_path": ".gitmodules", + "old_path": ".gitmodules", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "new_path": "files/.DS_Store", + "old_path": "files/.DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", + "new_path": "files/ruby/popen.rb", + "old_path": "files/ruby/popen.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "new_path": "files/ruby/regex.rb", + "old_path": "files/ruby/regex.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "new_path": "gitlab-grack", + "old_path": "gitlab-grack", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "new_path": "gitlab-shell", + "old_path": "gitlab-shell", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 1, + "created_at": "2016-04-12T13:26:49.896Z", + "updated_at": "2016-04-12T13:26:50.189Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "8" + }, + "notes": [ + { + "id": 2, + "note": ":+1: merge_request", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-04-12T13:26:56.790Z", + "updated_at": "2016-04-12T13:26:56.790Z", + "project_id": 9, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "commit_statuses": [ + { + "id": 1, + "project_id": null, + "status": "success", + "finished_at": "2016-01-26T07:23:42.000Z", + "trace": null, + "created_at": "2016-04-12T13:26:53.034Z", + "updated_at": "2016-04-12T13:26:53.850Z", + "started_at": "2016-01-26T07:21:42.000Z", + "runner_id": null, + "coverage": null, + "commit_id": 1, + "commands": null, + "job_id": null, + "name": "default", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": null, + "trigger_request_id": null, + "stage_idx": null, + "tag": null, + "ref": null, + "user_id": null, + "target_url": null, + "description": "commit status", + "artifacts_file": null, + "gl_project_id": 7, + "artifacts_metadata": null, + "erased_by_id": null, + "erased_at": null, + "commit": { + "id": 1, + "project_id": 6, + "ref": null, + "sha": "97de212e80737a608d939f648d959671fb0a0142", + "before_sha": null, + "push_data": null, + "created_at": "2016-04-12T13:26:53.010Z", + "updated_at": "2016-04-12T13:26:53.010Z", + "tag": false, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 6 + } + } + ] +} \ No newline at end of file -- cgit v1.2.1 From 6f74c7d4827955d44f53eeef0701e4e3c0f718a1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 12 Apr 2016 18:06:57 +0200 Subject: fix export commits stuff --- app/services/projects/import_export/import_export.yml | 4 ++-- app/services/projects/import_export/project_tree_saver.rb | 2 +- .../projects/import_export/project_tree_saver_spec.rb | 15 ++++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml index c622447ef04..92f492e9013 100644 --- a/app/services/projects/import_export/import_export.yml +++ b/app/services/projects/import_export/import_export.yml @@ -12,8 +12,8 @@ - :merge_requests: - :merge_request_diff - :notes - - :commit_statuses: - - :commit + - :ci_commits: + - :statuses :attributes_only: :project: diff --git a/app/services/projects/import_export/project_tree_saver.rb b/app/services/projects/import_export/project_tree_saver.rb index b2ca85bd7c6..37bf3e8e545 100644 --- a/app/services/projects/import_export/project_tree_saver.rb +++ b/app/services/projects/import_export/project_tree_saver.rb @@ -20,7 +20,7 @@ module Projects File.write(full_path, project_json_tree) true rescue - #TODO: handle error + # TODO: handle error false end diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb index eda9e08cdce..05ece413410 100644 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ b/spec/services/projects/import_export/project_tree_saver_spec.rb @@ -20,9 +20,10 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do merge_requests: [merge_request], labels: [label], snippets: [snippet], - releases: [release], - commit_statuses: [commit_status]) + releases: [release] + ) end + let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) } let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } @@ -86,10 +87,6 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues'].first['notes']).not_to be_empty end - it 'has commit statuses' do - expect(saved_project_json['commit_statuses']).not_to be_empty - end - it 'has project members' do expect(saved_project_json['project_members']).not_to be_empty end @@ -102,8 +99,12 @@ describe Projects::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty end + it 'has commit statuses' do + expect(saved_project_json['ci_commits'].first['statuses']).not_to be_empty + end + it 'has ci commits' do - expect(saved_project_json['commit_statuses'].first['commit']).not_to be_empty + expect(saved_project_json['ci_commits']).not_to be_empty end end end -- cgit v1.2.1 From 267fd01ce8fa1620d5fd597825c653630909d415 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 13 Apr 2016 17:47:14 +0200 Subject: fixed import and spec is now passing! --- .../import_export/project_tree_restorer.rb | 3 +- .../projects/import_export/relation_factory.rb | 21 +-- fixtures/import_export/project.json | 170 +++++++++++---------- 3 files changed, 99 insertions(+), 95 deletions(-) diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb index 2d9dee1031a..62d3e06fdcd 100644 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ b/app/services/projects/import_export/project_tree_restorer.rb @@ -25,14 +25,13 @@ module Projects def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) saved = [] relation_list.each do |relation| + next if !relation.is_a?(Hash) && tree_hash[relation.to_s].blank? if relation.is_a?(Hash) create_sub_relations(relation, tree_hash) end relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) saved << project.update_attribute(relation_key, relation_hash) - # FIXME - # next if tree_hash[relation.to_s].blank? end saved.all? end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb index 29917fce2ba..8fce8757228 100644 --- a/app/services/projects/import_export/relation_factory.rb +++ b/app/services/projects/import_export/relation_factory.rb @@ -3,27 +3,21 @@ module Projects module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets, commit: 'Ci::Commit' }.freeze + OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze def create(relation_sym:, relation_hash:, members_map:) relation_sym = parse_relation_sym(relation_sym) klass = parse_relation(relation_hash, relation_sym) - handle_merge_requests(relation_hash) if relation_sym == :merge_requests update_user_references(relation_hash, members_map) + update_project_references(relation_hash, klass) imported_object(klass, relation_hash) end private - def handle_merge_requests(relation_hash) - relation_hash['target_project_id'] = relation_hash.delete('project_id') - relation_hash['source_project_id'] = -1 - end - - #TODO nice to have, optimize this to only get called for specific models def update_user_references(relation_hash, members_map) USER_REFERENCES.each do |reference| if relation_hash[reference] @@ -32,6 +26,16 @@ module Projects end end + def update_project_references(relation_hash, klass) + project_id = relation_hash.delete('project_id') + + # project_id may not be part of the export, but we always need to populate it if required. + relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') + relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] + relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + relation_hash['source_project_id'] = -1 if relation_hash['source_project_id'] + end + def relation_class(relation_sym) relation_sym.to_s.classify.constantize end @@ -49,7 +53,6 @@ module Projects def parse_relation(relation_hash, relation_sym) klass = relation_class(relation_sym) relation_hash.delete('id') - relation_hash.delete('project_id') unless klass.column_names.include?('project_id') klass end end diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json index a2fed10b85e..4a12c951dcb 100644 --- a/fixtures/import_export/project.json +++ b/fixtures/import_export/project.json @@ -12,12 +12,12 @@ "issues": [ { "id": 1, - "title": "Ad est est quia inventore eius suscipit molestiae.", + "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", "assignee_id": 1, "author_id": 2, - "project_id": 7, - "created_at": "2016-04-12T13:26:48.974Z", - "updated_at": "2016-04-12T13:26:55.496Z", + "project_id": 6, + "created_at": "2016-04-13T14:40:32.471Z", + "updated_at": "2016-04-13T14:40:38.956Z", "position": 0, "branch_name": null, "description": null, @@ -34,8 +34,8 @@ "note": ":+1: issue", "noteable_type": "Issue", "author_id": 21, - "created_at": "2016-04-12T13:26:55.480Z", - "updated_at": "2016-04-12T13:26:55.480Z", + "created_at": "2016-04-13T14:40:38.944Z", + "updated_at": "2016-04-13T14:40:38.944Z", "project_id": 8, "attachment": { "url": null @@ -56,9 +56,9 @@ "id": 1, "title": "label1", "color": "#990000", - "project_id": 7, - "created_at": "2016-04-12T13:26:51.027Z", - "updated_at": "2016-04-12T13:26:53.799Z", + "project_id": 6, + "created_at": "2016-04-13T14:40:34.704Z", + "updated_at": "2016-04-13T14:40:36.891Z", "template": false, "description": null } @@ -67,11 +67,11 @@ { "id": 1, "title": "Milestone v1.2", - "project_id": 7, + "project_id": 6, "description": null, "due_date": null, - "created_at": "2016-04-12T13:26:53.993Z", - "updated_at": "2016-04-12T13:26:53.993Z", + "created_at": "2016-04-13T14:40:37.901Z", + "updated_at": "2016-04-13T14:40:37.901Z", "state": "active", "iid": 1 } @@ -79,13 +79,13 @@ "snippets": [ { "id": 1, - "title": "Possimus harum est mollitia fugiat in.", - "content": "Itaque ipsum culpa quibusdam mollitia.", + "title": "Illo ipsa maxime magni aut.", + "content": "Excepturi delectus ut harum est molestiae dolor.", "author_id": 10, - "project_id": 7, - "created_at": "2016-04-12T13:26:51.821Z", - "updated_at": "2016-04-12T13:26:53.811Z", - "file_name": "thomas_marquardt", + "project_id": 6, + "created_at": "2016-04-13T14:40:35.603Z", + "updated_at": "2016-04-13T14:40:36.903Z", + "file_name": "daphne.mraz", "visibility_level": 0 } ], @@ -94,9 +94,9 @@ "id": 1, "tag": "v1.1.0", "description": "Awesome release", - "project_id": 7, - "created_at": "2016-04-12T13:26:52.353Z", - "updated_at": "2016-04-12T13:26:53.823Z" + "project_id": 6, + "created_at": "2016-04-13T14:40:36.223Z", + "updated_at": "2016-04-13T14:40:36.913Z" } ], "events": [ @@ -106,9 +106,9 @@ "target_id": null, "title": null, "data": null, - "project_id": 7, - "created_at": "2016-04-12T13:26:57.139Z", - "updated_at": "2016-04-12T13:26:57.139Z", + "project_id": 6, + "created_at": "2016-04-13T14:40:40.122Z", + "updated_at": "2016-04-13T14:40:40.122Z", "action": 8, "author_id": 1 } @@ -118,19 +118,19 @@ "id": 1, "user": { "id": 1, - "email": "maybell_auer@gleasonolson.com", - "created_at": "2016-04-12T13:26:47.499Z", - "updated_at": "2016-04-12T13:26:47.499Z", - "name": "Charles Nitzsche", + "email": "norval.gulgowski@schambergerboyle.co.uk", + "created_at": "2016-04-13T14:40:30.963Z", + "updated_at": "2016-04-13T14:40:30.963Z", + "name": "Jalon Cormier DVM", "admin": false, "projects_limit": 42, "skype": "", "linkedin": "", "twitter": "", - "authentication_token": "nCBdoTPWP-5adipsFwEq", + "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", "theme_id": 2, "bio": null, - "username": "koby.zieme1", + "username": "vance.turner1", "can_create_group": true, "can_create_team": false, "state": "active", @@ -144,7 +144,7 @@ }, "hide_no_ssh_key": false, "website_url": "", - "notification_email": "maybell_auer@gleasonolson.com", + "notification_email": "norval.gulgowski@schambergerboyle.co.uk", "hide_no_password": false, "password_automatically_set": false, "location": null, @@ -173,13 +173,13 @@ "source_project_id": 2, "author_id": 5, "assignee_id": null, - "title": "Natus vel molestiae ab et dolorem odit ut.", - "created_at": "2016-04-12T13:26:49.813Z", - "updated_at": "2016-04-12T13:26:56.806Z", + "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", + "created_at": "2016-04-13T14:40:33.381Z", + "updated_at": "2016-04-13T14:40:39.850Z", "milestone_id": null, "state": "opened", "merge_status": "can_be_merged", - "target_project_id": 7, + "target_project_id": 6, "iid": 1, "description": null, "position": 0, @@ -353,8 +353,8 @@ } ], "merge_request_id": 1, - "created_at": "2016-04-12T13:26:49.896Z", - "updated_at": "2016-04-12T13:26:50.189Z", + "created_at": "2016-04-13T14:40:33.474Z", + "updated_at": "2016-04-13T14:40:33.834Z", "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", "real_size": "8" }, @@ -364,8 +364,8 @@ "note": ":+1: merge_request", "noteable_type": "MergeRequest", "author_id": 24, - "created_at": "2016-04-12T13:26:56.790Z", - "updated_at": "2016-04-12T13:26:56.790Z", + "created_at": "2016-04-13T14:40:39.832Z", + "updated_at": "2016-04-13T14:40:39.832Z", "project_id": 9, "attachment": { "url": null @@ -381,52 +381,54 @@ ] } ], - "commit_statuses": [ + "ci_commits": [ { - "id": 1, - "project_id": null, - "status": "success", - "finished_at": "2016-01-26T07:23:42.000Z", - "trace": null, - "created_at": "2016-04-12T13:26:53.034Z", - "updated_at": "2016-04-12T13:26:53.850Z", - "started_at": "2016-01-26T07:21:42.000Z", - "runner_id": null, - "coverage": null, - "commit_id": 1, - "commands": null, - "job_id": null, - "name": "default", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": null, - "trigger_request_id": null, - "stage_idx": null, - "tag": null, - "ref": null, - "user_id": null, - "target_url": null, - "description": "commit status", - "artifacts_file": null, - "gl_project_id": 7, - "artifacts_metadata": null, - "erased_by_id": null, - "erased_at": null, - "commit": { - "id": 1, - "project_id": 6, - "ref": null, - "sha": "97de212e80737a608d939f648d959671fb0a0142", - "before_sha": null, - "push_data": null, - "created_at": "2016-04-12T13:26:53.010Z", - "updated_at": "2016-04-12T13:26:53.010Z", - "tag": false, - "yaml_errors": null, - "committed_at": null, - "gl_project_id": 6 - } + "id": 2, + "project_id": 6, + "ref": "master", + "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "before_sha": null, + "push_data": null, + "created_at": "2016-04-13T14:40:37.759Z", + "updated_at": "2016-04-13T14:40:37.759Z", + "tag": false, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 6, + "statuses": [ + { + "id": 1, + "project_id": null, + "status": "success", + "finished_at": "2016-01-26T07:23:42.000Z", + "trace": null, + "created_at": "2016-04-13T14:40:37.717Z", + "updated_at": "2016-04-13T14:40:37.771Z", + "started_at": "2016-01-26T07:21:42.000Z", + "runner_id": null, + "coverage": null, + "commit_id": 2, + "commands": null, + "job_id": null, + "name": "default", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": null, + "trigger_request_id": null, + "stage_idx": null, + "tag": null, + "ref": null, + "user_id": null, + "target_url": null, + "description": "commit status", + "artifacts_file": null, + "gl_project_id": 7, + "artifacts_metadata": null, + "erased_by_id": null, + "erased_at": null + } + ] } ] } \ No newline at end of file -- cgit v1.2.1 From 0852f539aa389c66ef377b7d567c931f928e147f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 14 Apr 2016 16:57:25 +0200 Subject: refactored stuff, added a save and compress all class and moved mostly everything to lib --- app/services/projects/import_export.rb | 23 ----- .../projects/import_export/command_line_util.rb | 17 --- .../projects/import_export/export_service.rb | 11 +- .../projects/import_export/import_export.yml | 37 ------- .../projects/import_export/import_export_reader.rb | 91 ---------------- .../projects/import_export/project_tree_saver.rb | 37 ------- .../projects/import_export/repo_bundler.rb | 38 ------- app/services/projects/import_export/shared.rb | 13 --- .../projects/import_export/wiki_repo_bundler.rb | 34 ------ lib/gitlab/import_export.rb | 23 +++++ lib/gitlab/import_export/command_line_util.rb | 25 +++++ lib/gitlab/import_export/import_export.yml | 37 +++++++ lib/gitlab/import_export/import_export_reader.rb | 91 ++++++++++++++++ lib/gitlab/import_export/project_tree_saver.rb | 36 +++++++ lib/gitlab/import_export/repo_bundler.rb | 38 +++++++ lib/gitlab/import_export/saver.rb | 38 +++++++ lib/gitlab/import_export/shared.rb | 13 +++ lib/gitlab/import_export/wiki_repo_bundler.rb | 34 ++++++ .../import_export/import_export_reader_spec.rb | 24 +++++ .../import_export/project_tree_saver_spec.rb | 115 +++++++++++++++++++++ spec/lib/gitlab/import_export/repo_bundler_spec.rb | 25 +++++ .../gitlab/import_export/wiki_repo_bundler_spec.rb | 28 +++++ .../import_export/import_export_reader_spec.rb | 24 ----- .../import_export/project_tree_saver_spec.rb | 115 --------------------- .../projects/import_export/repo_bundler_spec.rb | 25 ----- .../import_export/wiki_repo_bundler_spec.rb | 28 ----- 26 files changed, 535 insertions(+), 485 deletions(-) delete mode 100644 app/services/projects/import_export.rb delete mode 100644 app/services/projects/import_export/command_line_util.rb delete mode 100644 app/services/projects/import_export/import_export.yml delete mode 100644 app/services/projects/import_export/import_export_reader.rb delete mode 100644 app/services/projects/import_export/project_tree_saver.rb delete mode 100644 app/services/projects/import_export/repo_bundler.rb delete mode 100644 app/services/projects/import_export/shared.rb delete mode 100644 app/services/projects/import_export/wiki_repo_bundler.rb create mode 100644 lib/gitlab/import_export.rb create mode 100644 lib/gitlab/import_export/command_line_util.rb create mode 100644 lib/gitlab/import_export/import_export.yml create mode 100644 lib/gitlab/import_export/import_export_reader.rb create mode 100644 lib/gitlab/import_export/project_tree_saver.rb create mode 100644 lib/gitlab/import_export/repo_bundler.rb create mode 100644 lib/gitlab/import_export/saver.rb create mode 100644 lib/gitlab/import_export/shared.rb create mode 100644 lib/gitlab/import_export/wiki_repo_bundler.rb create mode 100644 spec/lib/gitlab/import_export/import_export_reader_spec.rb create mode 100644 spec/lib/gitlab/import_export/project_tree_saver_spec.rb create mode 100644 spec/lib/gitlab/import_export/repo_bundler_spec.rb create mode 100644 spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb delete mode 100644 spec/services/projects/import_export/import_export_reader_spec.rb delete mode 100644 spec/services/projects/import_export/project_tree_saver_spec.rb delete mode 100644 spec/services/projects/import_export/repo_bundler_spec.rb delete mode 100644 spec/services/projects/import_export/wiki_repo_bundler_spec.rb diff --git a/app/services/projects/import_export.rb b/app/services/projects/import_export.rb deleted file mode 100644 index c74f1d31490..00000000000 --- a/app/services/projects/import_export.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Projects - module ImportExport - extend self - - def export_path(relative_path:) - File.join(storage_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export/#{relative_path}") - end - - def project_atts - %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) - end - - def project_tree - Projects::ImportExport::ImportExportReader.project_tree - end - - private - - def storage_path - File.join(Settings.shared['path'], 'tmp/project_exports') - end - end -end diff --git a/app/services/projects/import_export/command_line_util.rb b/app/services/projects/import_export/command_line_util.rb deleted file mode 100644 index 3ca49e76c7f..00000000000 --- a/app/services/projects/import_export/command_line_util.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Projects - module ImportExport - module CommandLineUtil - def tar_cf(archive:, dir:) - cmd = %W(tar -cf #{archive} -C #{dir} .) - _output, status = Gitlab::Popen.popen(cmd) - status.zero? - end - - def git_bundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) - cmd = %W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all) - _output, status = Gitlab::Popen.popen(cmd) - status.zero? - end - end - end -end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 99aa8489f65..ce13942c5d7 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,19 +2,24 @@ module Projects module ImportExport class ExportService < BaseService def execute(options = {}) - @shared = Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) + @shared = Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) save_project_tree bundle_repo + save_all end private def save_project_tree - Projects::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save + Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save end def bundle_repo - Projects::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle + Gitlab::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle + end + + def save_all + Gitlab::ImportExport::Saver.save(storage_path: @shared.export_path) end end end diff --git a/app/services/projects/import_export/import_export.yml b/app/services/projects/import_export/import_export.yml deleted file mode 100644 index 92f492e9013..00000000000 --- a/app/services/projects/import_export/import_export.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Class relationships to be included in the project import/export -:project_tree: - - :issues: - - :notes - - :labels - - :milestones - - :snippets - - :releases - - :events - - :project_members: - - :user - - :merge_requests: - - :merge_request_diff - - :notes - - :ci_commits: - - :statuses - -:attributes_only: - :project: - - :name - - :path - - :description - - :issues_enabled - - :wall_enabled - - :merge_requests_enabled - - :wiki_enabled - - :snippets_enabled - - :visibility_level - - :archived - :user: - - :id - - :email - - :username - -:attributes_except: - :snippets: - - :expired_at \ No newline at end of file diff --git a/app/services/projects/import_export/import_export_reader.rb b/app/services/projects/import_export/import_export_reader.rb deleted file mode 100644 index 001d24af604..00000000000 --- a/app/services/projects/import_export/import_export_reader.rb +++ /dev/null @@ -1,91 +0,0 @@ -module Projects - module ImportExport - module ImportExportReader - extend self - - def project_tree - { only: atts_only[:project], include: build_hash(tree) } - end - - def tree - config[:project_tree] - end - - private - - def config - @config ||= YAML.load_file('app/services/projects/import_export/import_export.yml') - end - - def atts_only - config[:attributes_only] - end - - def atts_except - config[:attributes_except] - end - - def build_hash(array) - array.map do |el| - if el.is_a?(Hash) - process_include(el) - else - only_except_hash = check_only_and_except(el) - only_except_hash.empty? ? el : { el => only_except_hash } - end - end - end - - def process_include(hash, included_classes_hash = {}) - hash.values.flatten.each do |value| - current_key = hash.keys.first - value = process_current_class(hash, included_classes_hash, value) - if included_classes_hash[current_key] - add_class(current_key, included_classes_hash, value) - else - add_new_class(current_key, included_classes_hash, value) - end - end - included_classes_hash - end - - def process_current_class(hash, included_classes_hash, value) - value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value - only_except_hash = check_only_and_except(hash.keys.first) - included_classes_hash[hash.keys.first] ||= only_except_hash unless only_except_hash.empty? - value - end - - def add_new_class(current_key, included_classes_hash, value) - new_hash = { include: value } - new_hash.merge!(check_only_and_except(value)) - included_classes_hash[current_key] = new_hash - end - - def add_class(current_key, included_classes_hash, value) - only_except_hash = check_only_and_except(value) - value = { value => only_except_hash } unless only_except_hash.empty? - old_values = included_classes_hash[current_key][:include] - included_classes_hash[current_key][:include] = ([old_values] + [value]).compact.flatten - end - - def check_only_and_except(value) - check_only(value).merge(check_except(value)) - end - - def check_only(value) - key = key_from_hash(value) - atts_only[key].nil? ? {} : { only: atts_only[key] } - end - - def check_except(value) - key = key_from_hash(value) - atts_except[key].nil? ? {} : { except: atts_except[key] } - end - - def key_from_hash(value) - value.is_a?(Hash) ? value.keys.first : value - end - end - end -end diff --git a/app/services/projects/import_export/project_tree_saver.rb b/app/services/projects/import_export/project_tree_saver.rb deleted file mode 100644 index 37bf3e8e545..00000000000 --- a/app/services/projects/import_export/project_tree_saver.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Projects - module ImportExport - class ProjectTreeSaver - attr_reader :full_path - - def initialize(project: , shared: ) - @project = project - @export_path = shared.export_path - end - - def save - @full_path = File.join(@export_path, project_filename) - save_to_disk - end - - private - - def save_to_disk - FileUtils.mkdir_p(@export_path) - File.write(full_path, project_json_tree) - true - rescue - # TODO: handle error - false - end - - def project_filename - # TODO sanitize name - "#{@project.name}.json" - end - - def project_json_tree - @project.to_json(Projects::ImportExport.project_tree) - end - end - end -end diff --git a/app/services/projects/import_export/repo_bundler.rb b/app/services/projects/import_export/repo_bundler.rb deleted file mode 100644 index d43fb4ea09e..00000000000 --- a/app/services/projects/import_export/repo_bundler.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Projects - module ImportExport - class RepoBundler - include Projects::ImportExport::CommandLineUtil - - attr_reader :full_path - - def initialize(project: , shared: ) - @project = project - @export_path = shared.export_path - end - - def bundle - return false if @project.empty_repo? - @full_path = File.join(@export_path, project_filename) - bundle_to_disk - end - - private - - def bundle_to_disk - FileUtils.mkdir_p(@export_path) - tar_cf(archive: full_path, dir: path_to_repo) - rescue - #TODO: handle error - false - end - - def project_filename - "#{@project.name}.bundle" - end - - def path_to_repo - @project.repository.path_to_repo - end - end - end -end diff --git a/app/services/projects/import_export/shared.rb b/app/services/projects/import_export/shared.rb deleted file mode 100644 index 04fe01c0d6c..00000000000 --- a/app/services/projects/import_export/shared.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Projects - module ImportExport - class Shared - def initialize(opts) - @opts = opts - end - - def export_path - @export_path ||= Projects::ImportExport.export_path(relative_path: @opts[:relative_path]) - end - end - end -end diff --git a/app/services/projects/import_export/wiki_repo_bundler.rb b/app/services/projects/import_export/wiki_repo_bundler.rb deleted file mode 100644 index bf69936503d..00000000000 --- a/app/services/projects/import_export/wiki_repo_bundler.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Projects - module ImportExport - class WikiRepoBundler < RepoBundler - def bundle - @wiki = ProjectWiki.new(@project) - return false if !wiki? - @full_path = File.join(@export_path, project_filename) - bundle_to_disk - end - - def bundle_to_disk - FileUtils.mkdir_p(@export_path) - git_bundle(repo_path: path_to_repo, bundle_path: @full_path) - rescue - #TODO: handle error - false - end - - private - - def project_filename - "#{@project.name}.wiki.bundle" - end - - def path_to_repo - @wiki.repository.path_to_repo - end - - def wiki? - File.exists?(@wiki.repository.path_to_repo) && !@wiki.repository.empty? - end - end - end -end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb new file mode 100644 index 00000000000..fe88850c33d --- /dev/null +++ b/lib/gitlab/import_export.rb @@ -0,0 +1,23 @@ +module Gitlab + module ImportExport + extend self + + def export_path(relative_path:) + File.join(storage_path, relative_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export") + end + + def project_atts + %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) + end + + def project_tree + Gitlab::ImportExport::ImportExportReader.project_tree + end + + private + + def storage_path + File.join(Settings.shared['path'], 'tmp/project_exports') + end + end +end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb new file mode 100644 index 00000000000..7bf4b476b6c --- /dev/null +++ b/lib/gitlab/import_export/command_line_util.rb @@ -0,0 +1,25 @@ +module Gitlab + module ImportExport + module CommandLineUtil + def tar_cf(archive:, dir:) + tar_with_options(archive: archive, dir: dir, options: 'cf') + end + + def tar_czf(archive:, dir:) + tar_with_options(archive: archive, dir: dir, options: 'czf') + end + + def git_bundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + cmd = %W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end + + def tar_with_options(archive:, dir:, options:) + cmd = %W(tar -#{options} #{archive} -C #{dir} .) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end + end + end +end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml new file mode 100644 index 00000000000..92f492e9013 --- /dev/null +++ b/lib/gitlab/import_export/import_export.yml @@ -0,0 +1,37 @@ +# Class relationships to be included in the project import/export +:project_tree: + - :issues: + - :notes + - :labels + - :milestones + - :snippets + - :releases + - :events + - :project_members: + - :user + - :merge_requests: + - :merge_request_diff + - :notes + - :ci_commits: + - :statuses + +:attributes_only: + :project: + - :name + - :path + - :description + - :issues_enabled + - :wall_enabled + - :merge_requests_enabled + - :wiki_enabled + - :snippets_enabled + - :visibility_level + - :archived + :user: + - :id + - :email + - :username + +:attributes_except: + :snippets: + - :expired_at \ No newline at end of file diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb new file mode 100644 index 00000000000..717d3026f9e --- /dev/null +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -0,0 +1,91 @@ +module Gitlab + module ImportExport + module ImportExportReader + extend self + + def project_tree + { only: atts_only[:project], include: build_hash(tree) } + end + + def tree + config[:project_tree] + end + + private + + def config + @config ||= YAML.load_file('lib/gitlab/import_export/import_export.yml') + end + + def atts_only + config[:attributes_only] + end + + def atts_except + config[:attributes_except] + end + + def build_hash(array) + array.map do |el| + if el.is_a?(Hash) + process_include(el) + else + only_except_hash = check_only_and_except(el) + only_except_hash.empty? ? el : { el => only_except_hash } + end + end + end + + def process_include(hash, included_classes_hash = {}) + hash.values.flatten.each do |value| + current_key = hash.keys.first + value = process_current_class(hash, included_classes_hash, value) + if included_classes_hash[current_key] + add_class(current_key, included_classes_hash, value) + else + add_new_class(current_key, included_classes_hash, value) + end + end + included_classes_hash + end + + def process_current_class(hash, included_classes_hash, value) + value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value + only_except_hash = check_only_and_except(hash.keys.first) + included_classes_hash[hash.keys.first] ||= only_except_hash unless only_except_hash.empty? + value + end + + def add_new_class(current_key, included_classes_hash, value) + new_hash = { include: value } + new_hash.merge!(check_only_and_except(value)) + included_classes_hash[current_key] = new_hash + end + + def add_class(current_key, included_classes_hash, value) + only_except_hash = check_only_and_except(value) + value = { value => only_except_hash } unless only_except_hash.empty? + old_values = included_classes_hash[current_key][:include] + included_classes_hash[current_key][:include] = ([old_values] + [value]).compact.flatten + end + + def check_only_and_except(value) + check_only(value).merge(check_except(value)) + end + + def check_only(value) + key = key_from_hash(value) + atts_only[key].nil? ? {} : { only: atts_only[key] } + end + + def check_except(value) + key = key_from_hash(value) + atts_except[key].nil? ? {} : { except: atts_except[key] } + end + + def key_from_hash(value) + value.is_a?(Hash) ? value.keys.first : value + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb new file mode 100644 index 00000000000..b2615f8273b --- /dev/null +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -0,0 +1,36 @@ +module Gitlab + module ImportExport + class ProjectTreeSaver + attr_reader :full_path + + def initialize(project: , shared: ) + @project = project + @export_path = shared.export_path + end + + def save + @full_path = File.join(@export_path, project_filename) + save_to_disk + end + + private + + def save_to_disk + FileUtils.mkdir_p(@export_path) + File.write(full_path, project_json_tree) + true + rescue + # TODO: handle error + false + end + + def project_filename + "#{@project.name}.json" + end + + def project_json_tree + @project.to_json(Gitlab::ImportExport.project_tree) + end + end + end +end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb new file mode 100644 index 00000000000..7a1c2a12a53 --- /dev/null +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -0,0 +1,38 @@ +module Gitlab + module ImportExport + class RepoBundler + include Gitlab::ImportExport::CommandLineUtil + + attr_reader :full_path + + def initialize(project: , shared: ) + @project = project + @export_path = shared.export_path + end + + def bundle + return false if @project.empty_repo? + @full_path = File.join(@export_path, project_filename) + bundle_to_disk + end + + private + + def bundle_to_disk + FileUtils.mkdir_p(@export_path) + tar_cf(archive: full_path, dir: path_to_repo) + rescue + #TODO: handle error + false + end + + def project_filename + "#{@project.name}.bundle" + end + + def path_to_repo + @project.repository.path_to_repo + end + end + end +end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb new file mode 100644 index 00000000000..f26804d2402 --- /dev/null +++ b/lib/gitlab/import_export/saver.rb @@ -0,0 +1,38 @@ +module Gitlab + module ImportExport + class Saver + include Gitlab::ImportExport::CommandLineUtil + + def self.save(*args) + new(*args).save + end + + def initialize(storage_path:) + @storage_path = storage_path + end + + def save + if compress_and_save + remove_storage_path + archive_file + else + false + end + end + + private + + def compress_and_save + tar_czf(archive: archive_file, dir: @storage_path) + end + + def remove_storage_path + FileUtils.rm_rf(@storage_path) + end + + def archive_file + @archive_file ||= File.join(@storage_path, '..', 'project.tar.gz') + end + end + end +end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb new file mode 100644 index 00000000000..a4ec33a6cf7 --- /dev/null +++ b/lib/gitlab/import_export/shared.rb @@ -0,0 +1,13 @@ +module Gitlab + module ImportExport + class Shared + def initialize(opts) + @opts = opts + end + + def export_path + @export_path ||= Gitlab::ImportExport.export_path(relative_path: @opts[:relative_path]) + end + end + end +end diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb new file mode 100644 index 00000000000..9ef0febee54 --- /dev/null +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -0,0 +1,34 @@ +module Gitlab + module ImportExport + class WikiRepoBundler < RepoBundler + def bundle + @wiki = ProjectWiki.new(@project) + return false if !wiki? + @full_path = File.join(@export_path, project_filename) + bundle_to_disk + end + + def bundle_to_disk + FileUtils.mkdir_p(@export_path) + git_bundle(repo_path: path_to_repo, bundle_path: @full_path) + rescue + #TODO: handle error + false + end + + private + + def project_filename + "#{@project.name}.wiki.bundle" + end + + def path_to_repo + @wiki.repository.path_to_repo + end + + def wiki? + File.exists?(@wiki.repository.path_to_repo) && !@wiki.repository.empty? + end + end + end +end diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb new file mode 100644 index 00000000000..e71c7b60b80 --- /dev/null +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -0,0 +1,24 @@ +require 'rspec' + +describe Gitlab::ImportExport::ImportExportReader do + + let(:test_config) { 'spec/support/import_export/import_export.yml' } + let(:project_tree_hash) do + { + only: [:name, :path], + include: [:issues, :labels, + { merge_requests: { + only: [:id], + except: [:iid], + include: [:merge_request_diff, :merge_request_test] + } }, + { commit_statuses: { include: :commit } }] + } + end + + it 'should generate hash from project tree config' do + allow(described_class).to receive(:config).and_return(YAML.load_file(test_config)) + + expect(described_class.project_tree).to eq(project_tree_hash) + end +end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb new file mode 100644 index 00000000000..8a923dde467 --- /dev/null +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::ProjectTreeSaver, services: true do + describe :save do + + # TODO refactor this into a setup method + + let(:user) { create(:user) } + let(:issue) { create(:issue, assignee: user) } + let(:merge_request) { create(:merge_request) } + let(:label) { create(:label) } + let(:snippet) { create(:project_snippet) } + let(:commit_status) { create(:commit_status) } + let(:release) { create(:release) } + let!(:project) do + create(:project, + :public, + name: 'searchable_project', + issues: [issue], + merge_requests: [merge_request], + labels: [label], + snippets: [snippet], + releases: [release] + ) + end + let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) } + let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: shared) } + let!(:issue_note) { create(:note, note: ":+1: issue", noteable: issue) } + let!(:merge_request_note) { create(:note, note: ":+1: merge_request", noteable: merge_request) } + + before(:each) do + project.team << [user, :master] + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after(:each) do + FileUtils.rm_rf(export_path) + end + + it 'saves project successfully' do + expect(project_tree_saver.save).to be true + end + + context 'JSON' do + + let(:saved_project_json) do + project_tree_saver.save + project_json(project_tree_saver.full_path) + end + + it 'saves the correct json' do + expect(saved_project_json).to include({ "name" => project.name }) + end + + it 'has events' do + expect(saved_project_json['events']).not_to be_empty + end + + it 'has milestones' do + expect(saved_project_json['milestones']).not_to be_empty + end + + it 'has merge requests' do + expect(saved_project_json['merge_requests']).not_to be_empty + end + + it 'has labels' do + expect(saved_project_json['labels']).not_to be_empty + end + + it 'has snippets' do + expect(saved_project_json['snippets']).not_to be_empty + end + + it 'has releases' do + expect(saved_project_json['releases']).not_to be_empty + end + + it 'has issues' do + expect(saved_project_json['issues']).not_to be_empty + end + + it 'has issue comments' do + expect(saved_project_json['issues'].first['notes']).not_to be_empty + end + + it 'has project members' do + expect(saved_project_json['project_members']).not_to be_empty + end + + it 'has merge requests diffs' do + expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty + end + + it 'has merge requests comments' do + expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty + end + + it 'has commit statuses' do + expect(saved_project_json['ci_commits'].first['statuses']).not_to be_empty + end + + it 'has ci commits' do + expect(saved_project_json['ci_commits']).not_to be_empty + end + end + end + + def project_json(filename) + JSON.parse(IO.read(filename)) + end +end diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb new file mode 100644 index 00000000000..23447d878a8 --- /dev/null +++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::RepoBundler, services: true do + describe :bundle do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:bundler) { Gitlab::ImportExport::RepoBundler.new(project: project, shared: shared) } + + before(:each) do + project.team << [user, :master] + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after(:each) do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(bundler.bundle).to be true + end + end +end diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb new file mode 100644 index 00000000000..6cc95edc217 --- /dev/null +++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::WikiRepoBundler, services: true do + describe :bundle do + + let(:user) { create(:user) } + let!(:project) { create(:project, :public, name: 'searchable_project') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } + let(:wiki_bundler) { Gitlab::ImportExport::WikiRepoBundler.new(project: project, shared: shared) } + let!(:project_wiki) { ProjectWiki.new(project, user) } + + before(:each) do + project.team << [user, :master] + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + project_wiki.wiki + project_wiki.create_page("index", "test content") + end + + after(:each) do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(wiki_bundler.bundle).to be true + end + end +end diff --git a/spec/services/projects/import_export/import_export_reader_spec.rb b/spec/services/projects/import_export/import_export_reader_spec.rb deleted file mode 100644 index f825292ce7e..00000000000 --- a/spec/services/projects/import_export/import_export_reader_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rspec' - -describe Projects::ImportExport::ImportExportReader do - - let(:test_config) { 'spec/support/import_export/import_export.yml' } - let(:project_tree_hash) do - { - only: [:name, :path], - include: [:issues, :labels, - { merge_requests: { - only: [:id], - except: [:iid], - include: [:merge_request_diff, :merge_request_test] - } }, - { commit_statuses: { include: :commit } }] - } - end - - it 'should generate hash from project tree config' do - allow(described_class).to receive(:config).and_return(YAML.load_file(test_config)) - - expect(described_class.project_tree).to eq(project_tree_hash) - end -end diff --git a/spec/services/projects/import_export/project_tree_saver_spec.rb b/spec/services/projects/import_export/project_tree_saver_spec.rb deleted file mode 100644 index 05ece413410..00000000000 --- a/spec/services/projects/import_export/project_tree_saver_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -require 'spec_helper' - -describe Projects::ImportExport::ProjectTreeSaver, services: true do - describe :save do - - # TODO refactor this into a setup method - - let(:user) { create(:user) } - let(:issue) { create(:issue, assignee: user) } - let(:merge_request) { create(:merge_request) } - let(:label) { create(:label) } - let(:snippet) { create(:project_snippet) } - let(:commit_status) { create(:commit_status) } - let(:release) { create(:release) } - let!(:project) do - create(:project, - :public, - name: 'searchable_project', - issues: [issue], - merge_requests: [merge_request], - labels: [label], - snippets: [snippet], - releases: [release] - ) - end - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) } - let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } - let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:project_tree_saver) { Projects::ImportExport::ProjectTreeSaver.new(project: project, shared: shared) } - let!(:issue_note) { create(:note, note: ":+1: issue", noteable: issue) } - let!(:merge_request_note) { create(:note, note: ":+1: merge_request", noteable: merge_request) } - - before(:each) do - project.team << [user, :master] - allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) - end - - after(:each) do - FileUtils.rm_rf(export_path) - end - - it 'saves project successfully' do - expect(project_tree_saver.save).to be true - end - - context 'JSON' do - - let(:saved_project_json) do - project_tree_saver.save - project_json(project_tree_saver.full_path) - end - - it 'saves the correct json' do - expect(saved_project_json).to include({ "name" => project.name }) - end - - it 'has events' do - expect(saved_project_json['events']).not_to be_empty - end - - it 'has milestones' do - expect(saved_project_json['milestones']).not_to be_empty - end - - it 'has merge requests' do - expect(saved_project_json['merge_requests']).not_to be_empty - end - - it 'has labels' do - expect(saved_project_json['labels']).not_to be_empty - end - - it 'has snippets' do - expect(saved_project_json['snippets']).not_to be_empty - end - - it 'has releases' do - expect(saved_project_json['releases']).not_to be_empty - end - - it 'has issues' do - expect(saved_project_json['issues']).not_to be_empty - end - - it 'has issue comments' do - expect(saved_project_json['issues'].first['notes']).not_to be_empty - end - - it 'has project members' do - expect(saved_project_json['project_members']).not_to be_empty - end - - it 'has merge requests diffs' do - expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty - end - - it 'has merge requests comments' do - expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty - end - - it 'has commit statuses' do - expect(saved_project_json['ci_commits'].first['statuses']).not_to be_empty - end - - it 'has ci commits' do - expect(saved_project_json['ci_commits']).not_to be_empty - end - end - end - - def project_json(filename) - JSON.parse(IO.read(filename)) - end -end diff --git a/spec/services/projects/import_export/repo_bundler_spec.rb b/spec/services/projects/import_export/repo_bundler_spec.rb deleted file mode 100644 index 1f8ed41718f..00000000000 --- a/spec/services/projects/import_export/repo_bundler_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -describe Projects::ImportExport::RepoBundler, services: true do - describe :bundle do - - let(:user) { create(:user) } - let!(:project) { create(:project, :public, name: 'searchable_project') } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } - let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:bundler) { Projects::ImportExport::RepoBundler.new(project: project, shared: shared) } - - before(:each) do - project.team << [user, :master] - allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) - end - - after(:each) do - FileUtils.rm_rf(export_path) - end - - it 'bundles the repo successfully' do - expect(bundler.bundle).to be true - end - end -end diff --git a/spec/services/projects/import_export/wiki_repo_bundler_spec.rb b/spec/services/projects/import_export/wiki_repo_bundler_spec.rb deleted file mode 100644 index a589f81c5f9..00000000000 --- a/spec/services/projects/import_export/wiki_repo_bundler_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Projects::ImportExport::WikiRepoBundler, services: true do - describe :bundle do - - let(:user) { create(:user) } - let!(:project) { create(:project, :public, name: 'searchable_project') } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } - let(:shared) { Projects::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:wiki_bundler) { Projects::ImportExport::WikiRepoBundler.new(project: project, shared: shared) } - let!(:project_wiki) { ProjectWiki.new(project, user) } - - before(:each) do - project.team << [user, :master] - allow_any_instance_of(Projects::ImportExport).to receive(:storage_path).and_return(export_path) - project_wiki.wiki - project_wiki.create_page("index", "test content") - end - - after(:each) do - FileUtils.rm_rf(export_path) - end - - it 'bundles the repo successfully' do - expect(wiki_bundler.bundle).to be true - end - end -end -- cgit v1.2.1 From 97c3aff16fa94cee622cd00ffaa2e3a6469c1439 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 14 Apr 2016 17:10:57 +0200 Subject: refactored import stuff, moved to lib --- .../projects/import_export/import_service.rb | 2 +- .../projects/import_export/members_mapper.rb | 59 --------------- .../projects/import_export/project_factory.rb | 40 ---------- .../import_export/project_tree_restorer.rb | 87 ---------------------- .../projects/import_export/relation_factory.rb | 60 --------------- lib/gitlab/import_export/members_mapper.rb | 59 +++++++++++++++ lib/gitlab/import_export/project_factory.rb | 40 ++++++++++ lib/gitlab/import_export/project_tree_restorer.rb | 87 ++++++++++++++++++++++ lib/gitlab/import_export/relation_factory.rb | 60 +++++++++++++++ .../gitlab/import_export/members_mapper_spec.rb | 50 +++++++++++++ .../import_export/project_tree_restorer_spec.rb | 19 +++++ .../projects/import_export/members_mapper_spec.rb | 50 ------------- .../import_export/project_tree_restorer_spec.rb | 19 ----- 13 files changed, 316 insertions(+), 316 deletions(-) delete mode 100644 app/services/projects/import_export/members_mapper.rb delete mode 100644 app/services/projects/import_export/project_factory.rb delete mode 100644 app/services/projects/import_export/project_tree_restorer.rb delete mode 100644 app/services/projects/import_export/relation_factory.rb create mode 100644 lib/gitlab/import_export/members_mapper.rb create mode 100644 lib/gitlab/import_export/project_factory.rb create mode 100644 lib/gitlab/import_export/project_tree_restorer.rb create mode 100644 lib/gitlab/import_export/relation_factory.rb create mode 100644 spec/lib/gitlab/import_export/members_mapper_spec.rb create mode 100644 spec/lib/gitlab/import_export/project_tree_restorer_spec.rb delete mode 100644 spec/services/projects/import_export/members_mapper_spec.rb delete mode 100644 spec/services/projects/import_export/project_tree_restorer_spec.rb diff --git a/app/services/projects/import_export/import_service.rb b/app/services/projects/import_export/import_service.rb index 3efddf75e48..d188b2dc83b 100644 --- a/app/services/projects/import_export/import_service.rb +++ b/app/services/projects/import_export/import_service.rb @@ -8,7 +8,7 @@ module Projects private def restore_project_tree - Projects::ImportExport::ProjectTreeRestorer.new(path: @import_path).restore + Gitlab::ImportExport::ProjectTreeRestorer.new(path: @import_path).restore end def restore_repo diff --git a/app/services/projects/import_export/members_mapper.rb b/app/services/projects/import_export/members_mapper.rb deleted file mode 100644 index 6d49d901bf3..00000000000 --- a/app/services/projects/import_export/members_mapper.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Projects - module ImportExport - class MembersMapper - - def self.map(*args) - new(*args).map - end - - def initialize(exported_members:, user:, project_id:) - @exported_members = exported_members - @user = user - @project_id = project_id - end - - def map - @project_member_map = Hash.new(default_project_member) - @exported_members.each do |member| - existing_user = User.where(find_project_user_query(member)).first - assign_member(existing_user, member) if existing_user - end - @project_member_map - end - - private - - def assign_member(existing_user, member) - old_user_id = member['user']['id'] - member['user'] = existing_user - project_member = ProjectMember.new(member_hash(member)) - @project_member_map[old_user_id] = project_member.user.id if project_member.save - end - - def member_hash(member) - member.except('id').merge(source_id: @project_id) - end - - #TODO: If default, then we need to leave a comment 'Comment by ' on comments - def default_project_member - @default_project_member ||= - begin - default_member = ProjectMember.new(default_project_member_hash) - default_member.user.id if default_member.save - end - end - - def default_project_member_hash - { user: @user, access_level: ProjectMember::MASTER, source_id: @project_id } - end - - def find_project_user_query(member) - user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email'])) - end - - def user_arel - @user_arel ||= User.arel_table - end - end - end -end diff --git a/app/services/projects/import_export/project_factory.rb b/app/services/projects/import_export/project_factory.rb deleted file mode 100644 index 1ca0cfb8673..00000000000 --- a/app/services/projects/import_export/project_factory.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Projects - module ImportExport - module ProjectFactory - extend self - - def create(project_params:, user:) - project = Project.new(project_params.except('id')) - project.creator = user - check_namespace(project_params['namespace_id'], project, user) - end - - def check_namespace(namespace_id, project, user) - if namespace_id - # Find matching namespace and check if it allowed - # for current user if namespace_id passed. - unless allowed_namespace?(user, namespace_id) - project.namespace_id = nil - deny_namespace(project) - end - else - # Set current user namespace if namespace_id is nil - project.namespace_id = user.namespace_id - end - project - end - - private - - def allowed_namespace?(user, namespace_id) - namespace = Namespace.find_by(id: namespace_id) - user.can?(:create_projects, namespace) - end - - def deny_namespace(project) - project.errors.add(:namespace, "is not valid") - end - - end - end -end diff --git a/app/services/projects/import_export/project_tree_restorer.rb b/app/services/projects/import_export/project_tree_restorer.rb deleted file mode 100644 index 62d3e06fdcd..00000000000 --- a/app/services/projects/import_export/project_tree_restorer.rb +++ /dev/null @@ -1,87 +0,0 @@ -module Projects - module ImportExport - class ProjectTreeRestorer - attr_reader :project - - def initialize(path:, user:) - @path = path - @user = user - end - - def restore - json = IO.read(@path) - @tree_hash = ActiveSupport::JSON.decode(json) - @project_members = @tree_hash.delete('project_members') - create_relations - end - - private - - def members_map - @members ||= Projects::ImportExport::MembersMapper.map( - exported_members: @project_members, user: @user, project_id: project.id) - end - - def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) - saved = [] - relation_list.each do |relation| - next if !relation.is_a?(Hash) && tree_hash[relation.to_s].blank? - if relation.is_a?(Hash) - create_sub_relations(relation, tree_hash) - end - relation_key = relation.is_a?(Hash) ? relation.keys.first : relation - relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) - saved << project.update_attribute(relation_key, relation_hash) - end - saved.all? - end - - def default_relation_list - Projects::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } - end - - def project - @project ||= create_project - end - - def create_project - project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } - project = Projects::ImportExport::ProjectFactory.create( - project_params: project_params, user: @user) - project.save - project - end - - def create_sub_relations(relation, tree_hash) - tree_hash[relation.keys.first.to_s].each do |relation_item| - relation.values.flatten.each do |sub_relation| - relation_hash = relation_item[sub_relation.to_s] - next if relation_hash.blank? - process_sub_relation(relation_hash, relation_item, sub_relation) - end - end - end - - def process_sub_relation(relation_hash, relation_item, sub_relation) - sub_relation_object = nil - if relation_hash.is_a?(Array) - sub_relation_object = create_relation(sub_relation, relation_hash) - else - sub_relation_object = relation_from_factory(sub_relation, relation_hash) - end - relation_item[sub_relation.to_s] = sub_relation_object - end - - def create_relation(relation, relation_hash_list) - [relation_hash_list].flatten.map do |relation_hash| - relation_from_factory(relation, relation_hash) - end - end - - def relation_from_factory(relation, relation_hash) - Projects::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) - end - end - end -end diff --git a/app/services/projects/import_export/relation_factory.rb b/app/services/projects/import_export/relation_factory.rb deleted file mode 100644 index 8fce8757228..00000000000 --- a/app/services/projects/import_export/relation_factory.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Projects - module ImportExport - module RelationFactory - extend self - - OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze - USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze - - def create(relation_sym:, relation_hash:, members_map:) - relation_sym = parse_relation_sym(relation_sym) - klass = parse_relation(relation_hash, relation_sym) - - update_user_references(relation_hash, members_map) - update_project_references(relation_hash, klass) - - imported_object(klass, relation_hash) - end - - private - - def update_user_references(relation_hash, members_map) - USER_REFERENCES.each do |reference| - if relation_hash[reference] - relation_hash[reference] = members_map[relation_hash[reference]] - end - end - end - - def update_project_references(relation_hash, klass) - project_id = relation_hash.delete('project_id') - - # project_id may not be part of the export, but we always need to populate it if required. - relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') - relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] - relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] - relation_hash['source_project_id'] = -1 if relation_hash['source_project_id'] - end - - def relation_class(relation_sym) - relation_sym.to_s.classify.constantize - end - - def parse_relation_sym(relation_sym) - OVERRIDES[relation_sym] || relation_sym - end - - def imported_object(klass, relation_hash) - imported_object = klass.new(relation_hash) - imported_object.importing = true if imported_object.respond_to?(:importing) - imported_object - end - - def parse_relation(relation_hash, relation_sym) - klass = relation_class(relation_sym) - relation_hash.delete('id') - klass - end - end - end -end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb new file mode 100644 index 00000000000..d6124106f57 --- /dev/null +++ b/lib/gitlab/import_export/members_mapper.rb @@ -0,0 +1,59 @@ +module Gitlab + module ImportExport + class MembersMapper + + def self.map(*args) + new(*args).map + end + + def initialize(exported_members:, user:, project_id:) + @exported_members = exported_members + @user = user + @project_id = project_id + end + + def map + @project_member_map = Hash.new(default_project_member) + @exported_members.each do |member| + existing_user = User.where(find_project_user_query(member)).first + assign_member(existing_user, member) if existing_user + end + @project_member_map + end + + private + + def assign_member(existing_user, member) + old_user_id = member['user']['id'] + member['user'] = existing_user + project_member = ProjectMember.new(member_hash(member)) + @project_member_map[old_user_id] = project_member.user.id if project_member.save + end + + def member_hash(member) + member.except('id').merge(source_id: @project_id) + end + + #TODO: If default, then we need to leave a comment 'Comment by ' on comments + def default_project_member + @default_project_member ||= + begin + default_member = ProjectMember.new(default_project_member_hash) + default_member.user.id if default_member.save + end + end + + def default_project_member_hash + { user: @user, access_level: ProjectMember::MASTER, source_id: @project_id } + end + + def find_project_user_query(member) + user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email'])) + end + + def user_arel + @user_arel ||= User.arel_table + end + end + end +end diff --git a/lib/gitlab/import_export/project_factory.rb b/lib/gitlab/import_export/project_factory.rb new file mode 100644 index 00000000000..c7137844a0a --- /dev/null +++ b/lib/gitlab/import_export/project_factory.rb @@ -0,0 +1,40 @@ +module Gitlab + module ImportExport + module ProjectFactory + extend self + + def create(project_params:, user:) + project = Project.new(project_params.except('id')) + project.creator = user + check_namespace(project_params['namespace_id'], project, user) + end + + def check_namespace(namespace_id, project, user) + if namespace_id + # Find matching namespace and check if it allowed + # for current user if namespace_id passed. + unless allowed_namespace?(user, namespace_id) + project.namespace_id = nil + deny_namespace(project) + end + else + # Set current user namespace if namespace_id is nil + project.namespace_id = user.namespace_id + end + project + end + + private + + def allowed_namespace?(user, namespace_id) + namespace = Namespace.find_by(id: namespace_id) + user.can?(:create_projects, namespace) + end + + def deny_namespace(project) + project.errors.add(:namespace, "is not valid") + end + + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb new file mode 100644 index 00000000000..4c0f6a2267b --- /dev/null +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -0,0 +1,87 @@ +module Gitlab + module ImportExport + class ProjectTreeRestorer + attr_reader :project + + def initialize(path:, user:) + @path = path + @user = user + end + + def restore + json = IO.read(@path) + @tree_hash = ActiveSupport::JSON.decode(json) + @project_members = @tree_hash.delete('project_members') + create_relations + end + + private + + def members_map + @members ||= Gitlab::ImportExport::MembersMapper.map( + exported_members: @project_members, user: @user, project_id: project.id) + end + + def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) + saved = [] + relation_list.each do |relation| + next if !relation.is_a?(Hash) && tree_hash[relation.to_s].blank? + if relation.is_a?(Hash) + create_sub_relations(relation, tree_hash) + end + relation_key = relation.is_a?(Hash) ? relation.keys.first : relation + relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) + saved << project.update_attribute(relation_key, relation_hash) + end + saved.all? + end + + def default_relation_list + Gitlab::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } + end + + def project + @project ||= create_project + end + + def create_project + project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } + project = Gitlab::ImportExport::ProjectFactory.create( + project_params: project_params, user: @user) + project.save + project + end + + def create_sub_relations(relation, tree_hash) + tree_hash[relation.keys.first.to_s].each do |relation_item| + relation.values.flatten.each do |sub_relation| + relation_hash = relation_item[sub_relation.to_s] + next if relation_hash.blank? + process_sub_relation(relation_hash, relation_item, sub_relation) + end + end + end + + def process_sub_relation(relation_hash, relation_item, sub_relation) + sub_relation_object = nil + if relation_hash.is_a?(Array) + sub_relation_object = create_relation(sub_relation, relation_hash) + else + sub_relation_object = relation_from_factory(sub_relation, relation_hash) + end + relation_item[sub_relation.to_s] = sub_relation_object + end + + def create_relation(relation, relation_hash_list) + [relation_hash_list].flatten.map do |relation_hash| + relation_from_factory(relation, relation_hash) + end + end + + def relation_from_factory(relation, relation_hash) + Gitlab::ImportExport::RelationFactory.create( + relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) + end + end + end +end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb new file mode 100644 index 00000000000..dd992ef443e --- /dev/null +++ b/lib/gitlab/import_export/relation_factory.rb @@ -0,0 +1,60 @@ +module Gitlab + module ImportExport + module RelationFactory + extend self + + OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze + + def create(relation_sym:, relation_hash:, members_map:) + relation_sym = parse_relation_sym(relation_sym) + klass = parse_relation(relation_hash, relation_sym) + + update_user_references(relation_hash, members_map) + update_project_references(relation_hash, klass) + + imported_object(klass, relation_hash) + end + + private + + def update_user_references(relation_hash, members_map) + USER_REFERENCES.each do |reference| + if relation_hash[reference] + relation_hash[reference] = members_map[relation_hash[reference]] + end + end + end + + def update_project_references(relation_hash, klass) + project_id = relation_hash.delete('project_id') + + # project_id may not be part of the export, but we always need to populate it if required. + relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') + relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] + relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + relation_hash['source_project_id'] = -1 if relation_hash['source_project_id'] + end + + def relation_class(relation_sym) + relation_sym.to_s.classify.constantize + end + + def parse_relation_sym(relation_sym) + OVERRIDES[relation_sym] || relation_sym + end + + def imported_object(klass, relation_hash) + imported_object = klass.new(relation_hash) + imported_object.importing = true if imported_object.respond_to?(:importing) + imported_object + end + + def parse_relation(relation_hash, relation_sym) + klass = relation_class(relation_sym) + relation_hash.delete('id') + klass + end + end + end +end diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb new file mode 100644 index 00000000000..9175356c641 --- /dev/null +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::MembersMapper, services: true do + describe :map do + + let(:user) { create(:user) } + let(:project) { create(:project, :public, name: 'searchable_project') } + let(:user2) { create(:user) } + let(:exported_user_id) { 99 } + let(:exported_members) do + [{ + "id" => 2, + "access_level" => 40, + "source_id" => 14, + "source_type" => "Project", + "user_id" => 19, + "notification_level" => 3, + "created_at" => "2016-03-11T10:21:44.822Z", + "updated_at" => "2016-03-11T10:21:44.822Z", + "created_by_id" => nil, + "invite_email" => nil, + "invite_token" => nil, + "invite_accepted_at" => nil, + "user" => + { + "id" => exported_user_id, + "email" => user2.email, + "username" => user2.username + } + }] + end + + let(:members_mapper) do + Gitlab::ImportExport::MembersMapper.new( + exported_members: exported_members, user: user, project_id: project.id) + end + + it 'maps a project member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + + it 'defaults to importer project member if it does not exist' do + expect(members_mapper.map[-1]).to eq(user.id) + end + end + + def project_member_user_id(id) + members_mapper.map[id] + end +end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb new file mode 100644 index 00000000000..f3d3a57ddd7 --- /dev/null +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do + describe :restore do + + let(:user) { create(:user) } + let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json", user: user) } + + context 'JSON' do + let(:restored_project_json) do + project_tree_restorer.restore + end + + it 'restores models based on JSON' do + expect(restored_project_json).to be true + end + end + end +end diff --git a/spec/services/projects/import_export/members_mapper_spec.rb b/spec/services/projects/import_export/members_mapper_spec.rb deleted file mode 100644 index e222dd42053..00000000000 --- a/spec/services/projects/import_export/members_mapper_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Projects::ImportExport::MembersMapper, services: true do - describe :map do - - let(:user) { create(:user) } - let(:project) { create(:project, :public, name: 'searchable_project') } - let(:user2) { create(:user) } - let(:exported_user_id) { 99 } - let(:exported_members) do - [{ - "id" => 2, - "access_level" => 40, - "source_id" => 14, - "source_type" => "Project", - "user_id" => 19, - "notification_level" => 3, - "created_at" => "2016-03-11T10:21:44.822Z", - "updated_at" => "2016-03-11T10:21:44.822Z", - "created_by_id" => nil, - "invite_email" => nil, - "invite_token" => nil, - "invite_accepted_at" => nil, - "user" => - { - "id" => exported_user_id, - "email" => user2.email, - "username" => user2.username - } - }] - end - - let(:members_mapper) do - Projects::ImportExport::MembersMapper.new( - exported_members: exported_members, user: user, project_id: project.id) - end - - it 'maps a project member' do - expect(members_mapper.map[exported_user_id]).to eq(user2.id) - end - - it 'defaults to importer project member if it does not exist' do - expect(members_mapper.map[-1]).to eq(user.id) - end - end - - def project_member_user_id(id) - members_mapper.map[id] - end -end diff --git a/spec/services/projects/import_export/project_tree_restorer_spec.rb b/spec/services/projects/import_export/project_tree_restorer_spec.rb deleted file mode 100644 index 4c8d182a213..00000000000 --- a/spec/services/projects/import_export/project_tree_restorer_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe Projects::ImportExport::ProjectTreeRestorer, services: true do - describe :restore do - - let(:user) { create(:user) } - let(:project_tree_restorer) { Projects::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json", user: user) } - - context 'JSON' do - let(:restored_project_json) do - project_tree_restorer.restore - end - - it 'restores models based on JSON' do - expect(restored_project_json).to be true - end - end - end -end -- cgit v1.2.1 From 41163fd55251edbfa0d0dd341e34c878563e4981 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 14 Apr 2016 18:01:21 +0200 Subject: some experimental UI stuff to test export --- app/controllers/projects_controller.rb | 9 +++++++++ app/services/projects/import_export/export_service.rb | 1 + app/views/projects/edit.html.haml | 13 +++++++++++++ config/routes.rb | 1 + 4 files changed, 24 insertions(+) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3cc37e59855..39ae3659a82 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -186,6 +186,15 @@ class ProjectsController < Projects::ApplicationController ) end + def export + ::Projects::ImportExport::ExportService.new(@project, current_user).execute + + redirect_to( + project_path(@project), + notice: "Project export successfully started" + ) + end + def toggle_star current_user.toggle_star(@project) @project.reload diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index ce13942c5d7..8b641008a88 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -1,6 +1,7 @@ module Projects module ImportExport class ExportService < BaseService + def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) save_project_tree diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 6d872cd0b21..8097ef5d889 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -132,6 +132,19 @@ = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" + .panel.panel-default + .panel-heading Export + .errors-holder + .panel-body + %p + Downloads a compressed version of the project that can be imported. + %br + + .form-actions + = link_to 'Export', export_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-default" + + - if can? current_user, :archive_project, @project - if @project.archived? .panel.panel-success diff --git a/config/routes.rb b/config/routes.rb index 842fbb99843..55d3084ed67 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -433,6 +433,7 @@ Rails.application.routes.draw do post :housekeeping post :toggle_star post :markdown_preview + post :export get :autocomplete_sources get :activity end -- cgit v1.2.1 From 05edd5e6dc169e4987e5e98e9243fa85c5b6e4b8 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 15 Apr 2016 15:38:34 +0200 Subject: download export now working --- app/controllers/projects_controller.rb | 11 ++++++++++- app/views/projects/edit.html.haml | 7 +++++-- config/routes.rb | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 39ae3659a82..9fb4370ef90 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -190,11 +190,15 @@ class ProjectsController < Projects::ApplicationController ::Projects::ImportExport::ExportService.new(@project, current_user).execute redirect_to( - project_path(@project), + edit_project_path(@project), notice: "Project export successfully started" ) end + def download_export + send_file export_project_path, disposition: 'attachment' + end + def toggle_star current_user.toggle_star(@project) @project.reload @@ -256,4 +260,9 @@ class ProjectsController < Projects::ApplicationController def get_id project.repository.root_ref end + + def export_project_path + # TODO: move this, probably to ImportExport and refactor + File.join(Settings.shared['path'], 'tmp/project_exports', @project.path_with_namespace, 'project.tar.gz') + end end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8097ef5d889..37ac0aa1f58 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -133,7 +133,7 @@ method: :post, class: "btn btn-default" .panel.panel-default - .panel-heading Export + .panel-heading Export project .errors-holder .panel-body %p @@ -141,9 +141,12 @@ %br .form-actions - = link_to 'Export', export_namespace_project_path(@project.namespace, @project), + = link_to 'Generate new export', export_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" + = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-default" + - if can? current_user, :archive_project, @project - if @project.archived? diff --git a/config/routes.rb b/config/routes.rb index 55d3084ed67..21164ba0eda 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -434,6 +434,7 @@ Rails.application.routes.draw do post :toggle_star post :markdown_preview post :export + post :download_export get :autocomplete_sources get :activity end -- cgit v1.2.1 From ae777ea0618e8db8a552a831dc331fe7a7b1fe7a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 15 Apr 2016 18:14:28 +0200 Subject: WIP - importing file and repo --- .../projects/import_export/import_service.rb | 11 ++++++-- lib/gitlab/import_export/command_line_util.rb | 14 ++++++++++ lib/gitlab/import_export/importer.rb | 26 ++++++++++++++++++ lib/gitlab/import_export/repo_restorer.rb | 31 ++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 lib/gitlab/import_export/importer.rb create mode 100644 lib/gitlab/import_export/repo_restorer.rb diff --git a/app/services/projects/import_export/import_service.rb b/app/services/projects/import_export/import_service.rb index d188b2dc83b..b252692cd77 100644 --- a/app/services/projects/import_export/import_service.rb +++ b/app/services/projects/import_export/import_service.rb @@ -2,17 +2,24 @@ module Projects module ImportExport class ExportService < BaseService def execute(options = {}) + @import_path = options[:import_path] + restore_project_tree + restore_repo(project_tree.project) end private def restore_project_tree - Gitlab::ImportExport::ProjectTreeRestorer.new(path: @import_path).restore + project_tree.restore end - def restore_repo + def project_tree + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: @import_path, user: @current_user) + end + def restore_repo(project) + Gitlab::ImportExport::RepoRestorer.new(path: @import_path, project: project).restore end end end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 7bf4b476b6c..f9041e9f4e5 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -5,6 +5,14 @@ module Gitlab tar_with_options(archive: archive, dir: dir, options: 'cf') end + def untar_czf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'czf') + end + + def untar_cf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'cf') + end + def tar_czf(archive:, dir:) tar_with_options(archive: archive, dir: dir, options: 'czf') end @@ -20,6 +28,12 @@ module Gitlab _output, status = Gitlab::Popen.popen(cmd) status.zero? end + + def untar_with_options(archive:, dir:, options:) + cmd = %W(tar -#{options} #{archive)} -C #{dir}) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end end end end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb new file mode 100644 index 00000000000..79f54000bb0 --- /dev/null +++ b/lib/gitlab/import_export/importer.rb @@ -0,0 +1,26 @@ +module Gitlab + module ImportExport + class Importer + include Gitlab::ImportExport::CommandLineUtil + + def self.import(*args) + new(*args).import + end + + def initialize(archive_file:, storage_path:) + @archive_file = archive_file + @storage_path = storage_path + end + + def import + decompress_export + end + + private + + def decompress + untar_czf(archive: archive_file, dir: @storage_path) + end + end + end +end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb new file mode 100644 index 00000000000..42126cabd97 --- /dev/null +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -0,0 +1,31 @@ +module Gitlab + module ImportExport + class RepoRestorer + include Gitlab::ImportExport::CommandLineUtil + + def initialize(project: , path: ) + @project = project + @path = path + end + + def restore + return false unless File.exists?(@path) + # Move repos dir to 'repositories.old' dir + + FileUtils.mkdir_p(repos_path) + FileUtils.mkdir_p(path_to_repo) + untar_cf(archive: @path, dir: path_to_repo) + end + + private + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def path_to_repo + @project.repository.path_to_repo + end + end + end +end -- cgit v1.2.1 From 42567436863c96b7f184cc7a728b2da3d18852c8 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Apr 2016 12:18:11 +0200 Subject: refactored path stuff --- lib/gitlab/import_export.rb | 4 +--- lib/gitlab/import_export/saver.rb | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index fe88850c33d..539eae13f33 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,7 +3,7 @@ module Gitlab extend self def export_path(relative_path:) - File.join(storage_path, relative_path, "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_gitlab_export") + File.join(storage_path, relative_path) end def project_atts @@ -14,8 +14,6 @@ module Gitlab Gitlab::ImportExport::ImportExportReader.project_tree end - private - def storage_path File.join(Settings.shared['path'], 'tmp/project_exports') end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index f26804d2402..f87e0fdc7ea 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -31,7 +31,7 @@ module Gitlab end def archive_file - @archive_file ||= File.join(@storage_path, '..', 'project.tar.gz') + @archive_file ||= File.join(@storage_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") end end end -- cgit v1.2.1 From 8f973b8f6887da082c4e4c777dc3961fae32ab16 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Apr 2016 12:45:20 +0200 Subject: more refactoring - easier guessing path changes --- app/services/projects/import_export/import_service.rb | 12 ++++++++---- lib/gitlab/import_export/command_line_util.rb | 4 ++-- lib/gitlab/import_export/importer.rb | 8 ++++---- lib/gitlab/import_export/project_tree_restorer.rb | 2 +- lib/gitlab/import_export/repo_restorer.rb | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/services/projects/import_export/import_service.rb b/app/services/projects/import_export/import_service.rb index b252692cd77..20eda2f228d 100644 --- a/app/services/projects/import_export/import_service.rb +++ b/app/services/projects/import_export/import_service.rb @@ -2,8 +2,8 @@ module Projects module ImportExport class ExportService < BaseService def execute(options = {}) - - @import_path = options[:import_path] + archive_file = options[:archive_file] + Gitlab::ImportExport::Importer.import(archive_file: archive_file, storage_path: storage_path) restore_project_tree restore_repo(project_tree.project) end @@ -15,11 +15,15 @@ module Projects end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: @import_path, user: @current_user) + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) end def restore_repo(project) - Gitlab::ImportExport::RepoRestorer.new(path: @import_path, project: project).restore + Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project).restore + end + + def storage_path + @storage_path ||= Gitlab::ImportExport.export_path(relative_path: project.path_with_namespace) end end end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index f9041e9f4e5..3665e2edb7d 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -24,13 +24,13 @@ module Gitlab end def tar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} -C #{dir} .) + cmd = %W(tar -#{options} #{archive} -C #{dir}) _output, status = Gitlab::Popen.popen(cmd) status.zero? end def untar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive)} -C #{dir}) + cmd = %W(tar -#{options} #{archive} -C #{dir}) _output, status = Gitlab::Popen.popen(cmd) status.zero? end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 79f54000bb0..9f399845437 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -7,19 +7,19 @@ module Gitlab new(*args).import end - def initialize(archive_file:, storage_path:) + def initialize(archive_file: , storage_path:) @archive_file = archive_file @storage_path = storage_path end def import - decompress_export + decompress_archive end private - def decompress - untar_czf(archive: archive_file, dir: @storage_path) + def decompress_archive + untar_czf(archive: @archive_file, dir: @storage_path) end end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 4c0f6a2267b..4e0f555afe9 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -4,7 +4,7 @@ module Gitlab attr_reader :project def initialize(path:, user:) - @path = path + @path = File.join(path, 'project.json') @user = user end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 42126cabd97..47be303e22a 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,9 +3,9 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project: , path: ) + def initialize(project: , path:, bundler_file: ) @project = project - @path = path + @path = File.join(path, bundler_file) end def restore -- cgit v1.2.1 From fedfba55194ed51c6a7510c01fa94091e92c71cf Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Apr 2016 12:49:37 +0200 Subject: more refactoring - easier guessing path changes --- lib/gitlab/import_export/project_tree_saver.rb | 2 +- lib/gitlab/import_export/repo_bundler.rb | 2 +- lib/gitlab/import_export/wiki_repo_bundler.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index b2615f8273b..f6fab0fe22d 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -25,7 +25,7 @@ module Gitlab end def project_filename - "#{@project.name}.json" + "project.json" end def project_json_tree diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index 7a1c2a12a53..86c9501b708 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -27,7 +27,7 @@ module Gitlab end def project_filename - "#{@project.name}.bundle" + "project.bundle" end def path_to_repo diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb index 9ef0febee54..7821c671628 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -19,7 +19,7 @@ module Gitlab private def project_filename - "#{@project.name}.wiki.bundle" + "project.wiki.bundle" end def path_to_repo -- cgit v1.2.1 From acf297955a5546161ac5e52589ba4740f234a0ae Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Apr 2016 17:44:59 +0200 Subject: gitlab import UI - icon, file selector, etc... Also updated font-awesome and modified import source settings. --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/assets/javascripts/behaviors/toggler_behavior.coffee | 1 + app/controllers/application_controller.rb | 6 +++++- app/models/application_setting.rb | 2 +- app/views/projects/_project_import_form.html.haml | 8 ++++++++ app/views/projects/new.html.haml | 9 ++++++++- db/schema.rb | 4 ++-- lib/gitlab/current_settings.rb | 2 +- lib/gitlab/import_sources.rb | 1 + 10 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 app/views/projects/_project_import_form.html.haml diff --git a/Gemfile b/Gemfile index 67cc3f34b8c..f8bd26f36f3 100644 --- a/Gemfile +++ b/Gemfile @@ -213,7 +213,7 @@ gem 'jquery-turbolinks', '~> 2.1.0' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' -gem 'font-awesome-rails', '~> 4.2' +gem 'font-awesome-rails', '~> 4.6.1' gem 'gitlab_emoji', '~> 0.3.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index b00d7b35c84..7ce19ba7b70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -322,7 +322,7 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.5.0.1) + font-awesome-rails (4.6.1.0) railties (>= 3.2, < 5.1) foreman (0.78.0) thor (~> 0.19.1) @@ -931,7 +931,7 @@ DEPENDENCIES flay flog fog (~> 1.36.0) - font-awesome-rails (~> 4.2) + font-awesome-rails (~> 4.6.1) foreman fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee index 177b6918270..640cdfe15f6 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ b/app/assets/javascripts/behaviors/toggler_behavior.coffee @@ -7,6 +7,7 @@ $ -> # %div.js-toggle-content # $("body").on "click", ".js-toggle-button", (e) -> + console.log(e); $(@).find('i'). toggleClass('fa fa-chevron-down'). toggleClass('fa fa-chevron-up') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1c53b0b21a3..7afde111a34 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :abilities, :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -325,6 +325,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('git') end + def gitlab_project_import_enabled? + current_application_settings.import_sources.include?('gitlab_project') + end + def two_factor_authentication_required? current_application_settings.require_two_factor_authentication end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 36f88154232..4e2c7c82406 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -147,7 +147,7 @@ class ApplicationSetting < ActiveRecord::Base default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], - import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git', 'gitlab_project'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/app/views/projects/_project_import_form.html.haml b/app/views/projects/_project_import_form.html.haml new file mode 100644 index 00000000000..62d4b55424c --- /dev/null +++ b/app/views/projects/_project_import_form.html.haml @@ -0,0 +1,8 @@ +.form-group.import-url-data + = f.label :import_url, class: 'control-label' do + %span GitLab export file + .col-sm-10 + = f.file_field :file, class: '' + + .well.prepend-top-20 + The project must have a valid Gitlab export format \ No newline at end of file diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a4c6094c69a..cf24c4cacf3 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -88,10 +88,17 @@ - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do %i.fa.fa-git - %span Any repo by URL + %span Repo by URL + + - if gitlab_project_import_enabled? + = link_to "#", class: 'btn import_gitlab_project', onclick: '$(".js-toggle-content2").toggle();' do + %i.fa.fa-gitlab + %span GitLab project .js-toggle-content.hide = render "shared/import_form", f: f + .js-toggle-content2.hide + = render "project_import_form", f: f .prepend-botton-10 diff --git a/db/schema.rb b/db/schema.rb index 42457d92353..f7dfd296aea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -78,8 +78,8 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.integer "metrics_packet_size", default: 1 t.text "shared_runners_text" + t.integer "metrics_packet_size", default: 1 end create_table "audit_events", force: :cascade do |t| @@ -426,9 +426,9 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.string "state" t.integer "iid" t.integer "updated_by_id" - t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" + t.integer "moved_to_id" t.date "due_date" end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index f44d1b3a44e..688e780c13d 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -29,7 +29,7 @@ module Gitlab default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], - import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git', 'gitlab_project'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index ccfdfbe73e8..2b5658a8b64 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -21,6 +21,7 @@ module Gitlab 'Google Code' => 'google_code', 'FogBugz' => 'fogbugz', 'Any repo by URL' => 'git', + 'GitLab project' => 'gitlab_project' } end -- cgit v1.2.1 From e5a59331cf5221d39f9430480d449f30bbe8e63f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Apr 2016 17:56:20 +0200 Subject: remove debug statement --- app/assets/javascripts/behaviors/toggler_behavior.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee index 640cdfe15f6..177b6918270 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.coffee +++ b/app/assets/javascripts/behaviors/toggler_behavior.coffee @@ -7,7 +7,6 @@ $ -> # %div.js-toggle-content # $("body").on "click", ".js-toggle-button", (e) -> - console.log(e); $(@).find('i'). toggleClass('fa fa-chevron-down'). toggleClass('fa fa-chevron-up') -- cgit v1.2.1 From 10f1609535742173d8747d3a1097ed7e919fb9e2 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 25 Apr 2016 18:00:15 +0200 Subject: changes to be picked by the UI branch --- .../import/gitlab_project_controller.rb | 46 ++++++++++++++++++++++ .../projects/import_export/import_service.rb | 30 -------------- config/routes.rb | 6 +++ lib/gitlab/import_export/import_service.rb | 39 ++++++++++++++++++ 4 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 app/controllers/import/gitlab_project_controller.rb delete mode 100644 app/services/projects/import_export/import_service.rb create mode 100644 lib/gitlab/import_export/import_service.rb diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb new file mode 100644 index 00000000000..fa3e0c7876d --- /dev/null +++ b/app/controllers/import/gitlab_project_controller.rb @@ -0,0 +1,46 @@ +class Import::GitlabProjectController < Import::BaseController + before_action :verify_gitlab_project_import_enabled + before_action :gitlab_project_auth, except: :callback + + rescue_from OAuth::Error, with: :gitlab_project_unauthorized + + #TODO permissions stuff + + def callback + + redirect_to status_import_gitlab_project_url + end + + def status + @repos = client.projects + @incompatible_repos = client.incompatible_projects + + @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @file = params[:file] + # @project_name = + + repo_owner = current_user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) + end + + private + + def verify_gitlab_project_import_enabled + render_404 unless gitlab_project_import_enabled? + end +end diff --git a/app/services/projects/import_export/import_service.rb b/app/services/projects/import_export/import_service.rb deleted file mode 100644 index 20eda2f228d..00000000000 --- a/app/services/projects/import_export/import_service.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Projects - module ImportExport - class ExportService < BaseService - def execute(options = {}) - archive_file = options[:archive_file] - Gitlab::ImportExport::Importer.import(archive_file: archive_file, storage_path: storage_path) - restore_project_tree - restore_repo(project_tree.project) - end - - private - - def restore_project_tree - project_tree.restore - end - - def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) - end - - def restore_repo(project) - Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project).restore - end - - def storage_path - @storage_path ||= Gitlab::ImportExport.export_path(relative_path: project.path_with_namespace) - end - end - end -end diff --git a/config/routes.rb b/config/routes.rb index 79b62a0b1bb..df6116b6eaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -156,6 +156,12 @@ Rails.application.routes.draw do get :new_user_map, path: :user_map post :create_user_map, path: :user_map end + + resource :gitlab_project, only: [:create, :new], controller: :gitlab_projects do + get :status + get :callback + get :jobs + end end # diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb new file mode 100644 index 00000000000..978a581f57a --- /dev/null +++ b/lib/gitlab/import_export/import_service.rb @@ -0,0 +1,39 @@ +module Gitlab + module ImportExport + class ImportService + + def self.execute(*args) + new(args).execute + end + + def initialize(options = {}) + @archive_file = options[:archive_file] + @current_user = options[:owner] + end + + def execute + Gitlab::ImportExport::Importer.import(archive_file: @archive_file, storage_path: storage_path) + restore_project_tree + restore_repo(project_tree.project) + end + + private + + def restore_project_tree + project_tree.restore + end + + def project_tree + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) + end + + def restore_repo(project) + Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project).restore + end + + def storage_path + @storage_path ||= Gitlab::ImportExport.export_path(relative_path: project.path_with_namespace) + end + end + end +end -- cgit v1.2.1 From 28b0208e93cbc949e1c733c60ec577740d824623 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 26 Apr 2016 10:59:00 +0200 Subject: refactoring, mainly UI stuff to follow other import actions --- .../import/gitlab_project_controller.rb | 46 ------------------ .../import/gitlab_projects_controller.rb | 45 ++++++++++++++++++ app/views/import/gitlab_projects/new.html.haml | 17 +++++++ app/views/import/gitlab_projects/status.html.haml | 55 ++++++++++++++++++++++ app/views/projects/_project_import_form.html.haml | 8 ---- app/views/projects/new.html.haml | 4 +- config/routes.rb | 1 - 7 files changed, 118 insertions(+), 58 deletions(-) delete mode 100644 app/controllers/import/gitlab_project_controller.rb create mode 100644 app/controllers/import/gitlab_projects_controller.rb create mode 100644 app/views/import/gitlab_projects/new.html.haml create mode 100644 app/views/import/gitlab_projects/status.html.haml delete mode 100644 app/views/projects/_project_import_form.html.haml diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb deleted file mode 100644 index fa3e0c7876d..00000000000 --- a/app/controllers/import/gitlab_project_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Import::GitlabProjectController < Import::BaseController - before_action :verify_gitlab_project_import_enabled - before_action :gitlab_project_auth, except: :callback - - rescue_from OAuth::Error, with: :gitlab_project_unauthorized - - #TODO permissions stuff - - def callback - - redirect_to status_import_gitlab_project_url - end - - def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects - - @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } - end - - def jobs - jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) - render json: jobs - end - - def create - @file = params[:file] - # @project_name = - - repo_owner = current_user.username - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) - end - - private - - def verify_gitlab_project_import_enabled - render_404 unless gitlab_project_import_enabled? - end -end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb new file mode 100644 index 00000000000..c99c97893ad --- /dev/null +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -0,0 +1,45 @@ +class Import::GitlabProjectsController < Import::BaseController + before_action :verify_gitlab_project_import_enabled + #before_action :gitlab_project_auth, except: :callback + + rescue_from OAuth::Error, with: :gitlab_project_unauthorized + + #TODO permissions stuff + + def new + + end + + def status + @repos = client.projects + @incompatible_repos = client.incompatible_projects + + @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @file = params[:file] + # @project_name = + + repo_owner = current_user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) + end + + private + + def verify_gitlab_project_import_enabled + render_404 unless gitlab_project_import_enabled? + end +end diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml new file mode 100644 index 00000000000..64ea536698d --- /dev/null +++ b/app/views/import/gitlab_projects/new.html.haml @@ -0,0 +1,17 @@ +- page_title "FogBugz Import" +- header_title "Projects", root_path +%h3.page-title + %i.fa.fa-bug + Import projects from FogBugz +%hr + += form_tag status_import_gitlab_project_url, class: 'form-horizontal' do + %p + To get started you add your project export file below. + .form-group + = label_tag :file, class: 'control-label' do + %span GitLab export file + .col-sm-10 + = file_field_tag :file, class: '' + .form-actions + = submit_tag 'Continue to the next step', class: 'btn btn-create' diff --git a/app/views/import/gitlab_projects/status.html.haml b/app/views/import/gitlab_projects/status.html.haml new file mode 100644 index 00000000000..2b879f0c595 --- /dev/null +++ b/app/views/import/gitlab_projects/status.html.haml @@ -0,0 +1,55 @@ +- page_title "Gitlab_project import" +- header_title "Projects", root_path +%h3.page-title + %i.icon-gitlab.icon-gitlab-big + Import Gitlab projects + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th From Gitlab_project.org + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = link_to project.import_source, "https://gitlab_project.org/#{project.import_source}", target: "_blank" + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = link_to repo.full_name, "https://gitlab_project.org/#{repo.full_name}", target: "_blank" + %td.import-target + = repo.full_name + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +:javascript + new ImporterStatus("#{jobs_import_gitlab_project_path}", "#{import_gitlab_project_path}"); diff --git a/app/views/projects/_project_import_form.html.haml b/app/views/projects/_project_import_form.html.haml deleted file mode 100644 index 62d4b55424c..00000000000 --- a/app/views/projects/_project_import_form.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span GitLab export file - .col-sm-10 - = f.file_field :file, class: '' - - .well.prepend-top-20 - The project must have a valid Gitlab export format \ No newline at end of file diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cf24c4cacf3..d4bb57aafc7 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -91,14 +91,12 @@ %span Repo by URL - if gitlab_project_import_enabled? - = link_to "#", class: 'btn import_gitlab_project', onclick: '$(".js-toggle-content2").toggle();' do + = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project' do %i.fa.fa-gitlab %span GitLab project .js-toggle-content.hide = render "shared/import_form", f: f - .js-toggle-content2.hide - = render "project_import_form", f: f .prepend-botton-10 diff --git a/config/routes.rb b/config/routes.rb index b4e1abd2768..05e89e81500 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,7 +159,6 @@ Rails.application.routes.draw do resource :gitlab_project, only: [:create, :new], controller: :gitlab_projects do get :status - get :callback get :jobs end end -- cgit v1.2.1 From 1d4c3fa1507fdfdecdc15091ac8ef8d9f5fb63d9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 26 Apr 2016 13:01:51 +0200 Subject: more refactoring, now using custom job for processing project imports --- .../import/gitlab_projects_controller.rb | 4 ++-- app/models/project.rb | 10 ++++++++++ app/views/import/gitlab_projects/new.html.haml | 2 +- app/workers/project_import_worker.rb | 22 ++++++++++++++++++++++ config/routes.rb | 1 + 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 app/workers/project_import_worker.rb diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index c99c97893ad..0e94915765c 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -26,7 +26,7 @@ class Import::GitlabProjectsController < Import::BaseController end def create - @file = params[:file] + file = params[:file] # @project_name = repo_owner = current_user.username @@ -34,7 +34,7 @@ class Import::GitlabProjectsController < Import::BaseController namespace = get_or_create_namespace || (render and return) - @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) + @project = Project.create_from_import_job(current_user.id, File.expand_path(file.path)) end private diff --git a/app/models/project.rb b/app/models/project.rb index 0420c6a61ae..18e647045cd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -359,6 +359,16 @@ class Project < ActiveRecord::Base def visible_to_user(user) where(id: user.authorized_projects.select(:id).reorder(nil)) end + + def create_from_import_job(current_user_id:, tmp_file:) + job_id = ProjectImportWorker.perform_async(current_user_id, tmp_file) + + if job_id + Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + else + Rails.logger.error "Import job failed to start for #{path_with_namespace}" + end + end end def team diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 64ea536698d..9e798b8a63f 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -5,7 +5,7 @@ Import projects from FogBugz %hr -= form_tag status_import_gitlab_project_url, class: 'form-horizontal' do += form_tag import_gitlab_project_path, class: 'form-horizontal' do %p To get started you add your project export file below. .form-group diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb new file mode 100644 index 00000000000..6dd514b56a0 --- /dev/null +++ b/app/workers/project_import_worker.rb @@ -0,0 +1,22 @@ +class ProjectImportWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell + + def perform(current_user_id, tmp_file) + current_user = User.find(current_user_id) + + project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file, owner: current_user) + + # TODO: Move this to import service + # if result[:status] == :error + # project.update(import_error: result[:message]) + # project.import_fail + # return + # end + + project.repository.after_import + project.import_finish + end +end diff --git a/config/routes.rb b/config/routes.rb index 05e89e81500..99ce0118104 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,6 +159,7 @@ Rails.application.routes.draw do resource :gitlab_project, only: [:create, :new], controller: :gitlab_projects do get :status + post :create get :jobs end end -- cgit v1.2.1 From cbbc42e0c4e5888413a690a4e0760e3cadd55a63 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 26 Apr 2016 17:12:50 +0200 Subject: adding more UI changes, updated controller, worker and refactored import service --- app/controllers/import/gitlab_projects_controller.rb | 17 ++++++++++++----- app/models/project.rb | 8 ++++---- app/views/import/gitlab_projects/new.html.haml | 2 +- app/workers/project_import_worker.rb | 9 ++++++--- lib/gitlab/import_export/import_service.rb | 20 +++++++++++++------- lib/gitlab/import_export/project_tree_restorer.rb | 1 + 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 0e94915765c..36be79a39c4 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -7,7 +7,8 @@ class Import::GitlabProjectsController < Import::BaseController #TODO permissions stuff def new - + @namespace_id = project_params[:namespace_id] + @path = project_params[:path] end def status @@ -27,14 +28,14 @@ class Import::GitlabProjectsController < Import::BaseController def create file = params[:file] - # @project_name = repo_owner = current_user.username @target_namespace = params[:new_namespace].presence || repo_owner - namespace = get_or_create_namespace || (render and return) - - @project = Project.create_from_import_job(current_user.id, File.expand_path(file.path)) + @project = Project.create_from_import_job(current_user_id: current_user.id, + tmp_file: File.expand_path(file.path), + namespace_id: @namespace_id, + project_path: @path) end private @@ -42,4 +43,10 @@ class Import::GitlabProjectsController < Import::BaseController def verify_gitlab_project_import_enabled render_404 unless gitlab_project_import_enabled? end + + def project_params + params.require(:project).permit( + :path, :namespace_id, + ) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 18e647045cd..70921e02759 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -360,13 +360,13 @@ class Project < ActiveRecord::Base where(id: user.authorized_projects.select(:id).reorder(nil)) end - def create_from_import_job(current_user_id:, tmp_file:) - job_id = ProjectImportWorker.perform_async(current_user_id, tmp_file) + def create_from_import_job(current_user_id:, tmp_file:, namespace_id:, project_path:) + job_id = ProjectImportWorker.perform_async(current_user_id, tmp_file, namespace_id, project_path) if job_id - Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + Rails.logger.info "Import job started for export #{tmp_file} with job ID #{job_id}" else - Rails.logger.error "Import job failed to start for #{path_with_namespace}" + Rails.logger.error "Import job failed to start for #{tmp_file}" end end end diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 9e798b8a63f..323b7810c59 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -5,7 +5,7 @@ Import projects from FogBugz %hr -= form_tag import_gitlab_project_path, class: 'form-horizontal' do += form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do %p To get started you add your project export file below. .form-group diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb index 6dd514b56a0..0e8b9552442 100644 --- a/app/workers/project_import_worker.rb +++ b/app/workers/project_import_worker.rb @@ -2,12 +2,15 @@ class ProjectImportWorker include Sidekiq::Worker include Gitlab::ShellAdapter - sidekiq_options queue: :gitlab_shell + sidekiq_options queue: :gitlab_shell, retry: false - def perform(current_user_id, tmp_file) + def perform(current_user_id, tmp_file, namespace_id, path) current_user = User.find(current_user_id) - project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file, owner: current_user) + project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file, + owner: current_user, + namespace_id: namespace_id, + project_path: path) # TODO: Move this to import service # if result[:status] == :error diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 978a581f57a..5152d6ac182 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -6,15 +6,17 @@ module Gitlab new(args).execute end - def initialize(options = {}) - @archive_file = options[:archive_file] - @current_user = options[:owner] + def initialize(archive_file:, owner:, namespace_id:, project_path:) + @archive_file = archive_file + @current_user = owner + @namespace_path = Namespace.find(namespace_id).path + @project_path = project_path end def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, storage_path: storage_path) restore_project_tree - restore_repo(project_tree.project) + restore_repo end private @@ -27,12 +29,16 @@ module Gitlab @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) end - def restore_repo(project) - Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project).restore + def restore_repo + Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project_tree.project).restore end def storage_path - @storage_path ||= Gitlab::ImportExport.export_path(relative_path: project.path_with_namespace) + @storage_path ||= Gitlab::ImportExport.export_path(relative_path: path_with_namespace) + end + + def path_with_namespace + File.join(@namespace_path, @project_path) end end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 4e0f555afe9..445f1d884d0 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -49,6 +49,7 @@ module Gitlab project = Gitlab::ImportExport::ProjectFactory.create( project_params: project_params, user: @user) project.save + project.import_start project end -- cgit v1.2.1 From 34c826a3110700f401fac4242f6a5dd15884f33d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Apr 2016 15:01:55 +0200 Subject: some JS magic to pass namespace and path importing projects --- app/controllers/import/gitlab_projects_controller.rb | 9 ++++++--- app/views/import/gitlab_projects/new.html.haml | 3 +++ app/views/projects/new.html.haml | 6 +++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 36be79a39c4..b141b5f472a 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -27,15 +27,18 @@ class Import::GitlabProjectsController < Import::BaseController end def create + # TODO verify access to namespace and path file = params[:file] + namespace_id = project_params[:namespace_id] + path = project_params[:path] repo_owner = current_user.username @target_namespace = params[:new_namespace].presence || repo_owner @project = Project.create_from_import_job(current_user_id: current_user.id, tmp_file: File.expand_path(file.path), - namespace_id: @namespace_id, - project_path: @path) + namespace_id: namespace_id, + project_path: path) end private @@ -45,7 +48,7 @@ class Import::GitlabProjectsController < Import::BaseController end def project_params - params.require(:project).permit( + params.permit( :path, :namespace_id, ) end diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 323b7810c59..1158a04ea46 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -9,9 +9,12 @@ %p To get started you add your project export file below. .form-group + = hidden_field_tag :namespace_id, @namespace_id + = hidden_field_tag :path, @path = label_tag :file, class: 'control-label' do %span GitLab export file .col-sm-10 = file_field_tag :file, class: '' + .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index d4bb57aafc7..b3d755b3790 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -91,7 +91,7 @@ %span Repo by URL - if gitlab_project_import_enabled? - = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project' do + = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do %i.fa.fa-gitlab %span GitLab project @@ -127,3 +127,7 @@ $('.modal-header .close').bind('click', function() { $(".modal").hide(); }); + $('.import_gitlab_project').bind('click', function() { + var _href = $("a.import_gitlab_project").attr("href"); + $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); + }); -- cgit v1.2.1 From cec4ae55b728c76f797ada20125d5bbf2af0adb9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Apr 2016 18:07:40 +0200 Subject: quite a few fixes - import service --- lib/gitlab/import_export/command_line_util.rb | 12 +++--------- lib/gitlab/import_export/import_service.rb | 4 ++-- lib/gitlab/import_export/importer.rb | 1 + lib/gitlab/import_export/project_tree_restorer.rb | 4 +++- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 3665e2edb7d..e8d41a6bd3d 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -6,11 +6,11 @@ module Gitlab end def untar_czf(archive:, dir:) - untar_with_options(archive: archive, dir: dir, options: 'czf') + tar_with_options(archive: archive, dir: dir, options: 'czf') end def untar_cf(archive:, dir:) - untar_with_options(archive: archive, dir: dir, options: 'cf') + tar_with_options(archive: archive, dir: dir, options: 'cf') end def tar_czf(archive:, dir:) @@ -24,13 +24,7 @@ module Gitlab end def tar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} -C #{dir}) - _output, status = Gitlab::Popen.popen(cmd) - status.zero? - end - - def untar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} -C #{dir}) + cmd = %W(tar -#{options} #{archive} #{dir}) _output, status = Gitlab::Popen.popen(cmd) status.zero? end diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 5152d6ac182..226499030af 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -3,7 +3,7 @@ module Gitlab class ImportService def self.execute(*args) - new(args).execute + new(*args).execute end def initialize(archive_file:, owner:, namespace_id:, project_path:) @@ -26,7 +26,7 @@ module Gitlab end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user, project_path: @project_path) end def restore_repo diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 9f399845437..225e6f34991 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -13,6 +13,7 @@ module Gitlab end def import + FileUtils.mkdir_p(@storage_path) decompress_archive end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 445f1d884d0..7b41ba0685b 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -3,9 +3,10 @@ module Gitlab class ProjectTreeRestorer attr_reader :project - def initialize(path:, user:) + def initialize(path:, user:, project_path:) @path = File.join(path, 'project.json') @user = user + @project_path = project_path end def restore @@ -48,6 +49,7 @@ module Gitlab project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project = Gitlab::ImportExport::ProjectFactory.create( project_params: project_params, user: @user) + project.path = @project_path project.save project.import_start project -- cgit v1.2.1 From 5908bdf3edb6f59674789e3d210999406d455f67 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 28 Apr 2016 13:53:16 +0200 Subject: fixing a few tar issues - and using gnu tar only --- lib/gitlab/import_export/command_line_util.rb | 14 ++++++++++---- lib/gitlab/import_export/importer.rb | 2 +- lib/gitlab/import_export/repo_restorer.rb | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index e8d41a6bd3d..1140e7beb9a 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -5,12 +5,12 @@ module Gitlab tar_with_options(archive: archive, dir: dir, options: 'cf') end - def untar_czf(archive:, dir:) - tar_with_options(archive: archive, dir: dir, options: 'czf') + def untar_zxf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'zxf') end - def untar_cf(archive:, dir:) - tar_with_options(archive: archive, dir: dir, options: 'cf') + def untar_czf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'xf') end def tar_czf(archive:, dir:) @@ -28,6 +28,12 @@ module Gitlab _output, status = Gitlab::Popen.popen(cmd) status.zero? end + + def untar_with_options(archive:, dir:, options:) + cmd = %W(tar -#{options} #{archive} -C #{dir}) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end end end end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 225e6f34991..8f838287f97 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -20,7 +20,7 @@ module Gitlab private def decompress_archive - untar_czf(archive: @archive_file, dir: @storage_path) + untar_zxf(archive: @archive_file, dir: @storage_path) end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 47be303e22a..aa90f053680 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -14,7 +14,7 @@ module Gitlab FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) - untar_cf(archive: @path, dir: path_to_repo) + untar_czf(archive: @path, dir: path_to_repo) end private -- cgit v1.2.1 From c5bc262981a9fbfe748dfb0b527e4fb0fa597ccf Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 28 Apr 2016 18:00:30 +0200 Subject: few fixes - import from UI working --- app/controllers/projects_controller.rb | 4 +++- app/services/projects/import_export/export_service.rb | 2 +- lib/gitlab/import_export/command_line_util.rb | 2 +- lib/gitlab/import_export/saver.rb | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 1d4684ca22e..62f8b376c18 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -191,6 +191,7 @@ class ProjectsController < Projects::ApplicationController end def export + #TODO: Move to worker ::Projects::ImportExport::ExportService.new(@project, current_user).execute redirect_to( @@ -267,6 +268,7 @@ class ProjectsController < Projects::ApplicationController def export_project_path # TODO: move this, probably to ImportExport and refactor - File.join(Settings.shared['path'], 'tmp/project_exports', @project.path_with_namespace, 'project.tar.gz') + folder = File.join(Settings.shared['path'], 'tmp/project_exports', @project.path_with_namespace) + Dir.glob("#{folder}/*export.tar.gz").max_by {|f| File.ctime(f)} end end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 8b641008a88..5d5573cba5c 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -3,7 +3,7 @@ module Projects class ExportService < BaseService def execute(options = {}) - @shared = Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) + @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) save_project_tree bundle_repo save_all diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 1140e7beb9a..9fc83afc4f7 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -24,7 +24,7 @@ module Gitlab end def tar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} #{dir}) + cmd = %W(tar -#{options} #{archive} -C #{dir} .) _output, status = Gitlab::Popen.popen(cmd) status.zero? end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index f87e0fdc7ea..634e58e6039 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -14,6 +14,7 @@ module Gitlab def save if compress_and_save remove_storage_path + Rails.logger.info("Saved project export #{archive_file}") archive_file else false -- cgit v1.2.1 From c46f3bcb5184f93f6424efbd4a2117caa6b6193f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 29 Apr 2016 13:11:07 +0200 Subject: few fixes and new integration spec -WIP --- lib/gitlab/import_export/project_tree_restorer.rb | 1 - lib/gitlab/import_export/repo_restorer.rb | 2 +- .../projects/import_export/import_file_spec.rb | 22 +++++++++++++++++++++ .../import_export/test_project_export.tar.gz | Bin 0 -> 353039 bytes 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 spec/features/projects/import_export/import_file_spec.rb create mode 100644 spec/features/projects/import_export/test_project_export.tar.gz diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 7b41ba0685b..a5dc3a7c9c0 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -66,7 +66,6 @@ module Gitlab end def process_sub_relation(relation_hash, relation_item, sub_relation) - sub_relation_object = nil if relation_hash.is_a?(Array) sub_relation_object = create_relation(sub_relation, relation_hash) else diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index aa90f053680..aae65f6adc8 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -14,7 +14,7 @@ module Gitlab FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) - untar_czf(archive: @path, dir: path_to_repo) + untar_zxf(archive: @path, dir: path_to_repo) end private diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb new file mode 100644 index 00000000000..e40ed9a683a --- /dev/null +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'project import', feature: true, js: true do + include Select2Helper + + let(:user) { create(:user) } + let!(:namespace) { create(:namespace, name: "asd", owner: user) } + background do + login_as(user) + end + + scenario 'user imports an exported project successfully' do + visit new_project_path + + select2('asd', from: '#project_namespace_id') + fill_in :project_path, with:'test-project-path', visible: true + click_link 'GitLab project' + + expect(page).to have_content('GitLab export file') + expect(URI.parse(current_url).query).to eq('namespace_id=asd&path=test-project-path') + end +end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz new file mode 100644 index 00000000000..bc447b4a62a Binary files /dev/null and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 7ebf22e0024f457238090c3545b8acafae0e1a6f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 29 Apr 2016 15:15:27 +0200 Subject: more refactoring and fixes - fixing spec as well --- lib/gitlab/import_export/project_tree_restorer.rb | 10 +++---- lib/gitlab/import_export/repo_bundler.rb | 1 + lib/gitlab/import_export/repo_restorer.rb | 5 ++-- .../projects/import_export/import_file_spec.rb | 35 ++++++++++++++++++++-- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index a5dc3a7c9c0..d2834101758 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -1,7 +1,6 @@ module Gitlab module ImportExport class ProjectTreeRestorer - attr_reader :project def initialize(path:, user:, project_path:) @path = File.join(path, 'project.json') @@ -16,6 +15,10 @@ module Gitlab create_relations end + def project + @project ||= create_project + end + private def members_map @@ -41,17 +44,12 @@ module Gitlab Gitlab::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } end - def project - @project ||= create_project - end - def create_project project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project = Gitlab::ImportExport::ProjectFactory.create( project_params: project_params, user: @user) project.path = @project_path project.save - project.import_start project end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index 86c9501b708..e719ee2e9e3 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -26,6 +26,7 @@ module Gitlab false end + # TODO remove magic keyword and move it to a shared config def project_filename "project.bundle" end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index aae65f6adc8..2c67bd5a845 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,9 +3,10 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project: , path:, bundler_file: ) + def initialize(project: , path: ) @project = project - @path = File.join(path, bundler_file) + # TODO remove magic keyword and move it to a shared config + @path = File.join(path, 'project.bundle') end def restore diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index e40ed9a683a..32e131d7040 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -3,8 +3,10 @@ require 'spec_helper' feature 'project import', feature: true, js: true do include Select2Helper - let(:user) { create(:user) } + let(:user) { create(:admin) } let!(:namespace) { create(:namespace, name: "asd", owner: user) } + let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } + background do login_as(user) end @@ -12,11 +14,38 @@ feature 'project import', feature: true, js: true do scenario 'user imports an exported project successfully' do visit new_project_path - select2('asd', from: '#project_namespace_id') + select2('2', from: '#project_namespace_id') fill_in :project_path, with:'test-project-path', visible: true click_link 'GitLab project' expect(page).to have_content('GitLab export file') - expect(URI.parse(current_url).query).to eq('namespace_id=asd&path=test-project-path') + expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path') + attach_file('file', file) + + #TODO check timings + + sleep 1 + + click_on 'Continue to the next step' + + sleep 1 + + end + + def drop_in_dropzone(file_path) + # Generate a fake input selector + page.execute_script <<-JS + var fakeFileInput = window.$('').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + JS + # Attach the file to the fake input selector with Capybara + attach_file("fakeFileInput", file_path) + # Add the file to a fileList array and trigger the fake drop event + page.execute_script <<-JS + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); + JS end end -- cgit v1.2.1 From 07ab6c2e26a36734ad29ae037810c14d2be7ec8b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 2 May 2016 11:29:21 +0200 Subject: refactoring and fixing a bunch of stuff --- .../import/gitlab_projects_controller.rb | 8 +--- app/views/import/gitlab_projects/status.html.haml | 55 ---------------------- lib/gitlab/import_export/command_line_util.rb | 2 +- lib/gitlab/import_export/import_service.rb | 3 +- lib/gitlab/import_export/repo_restorer.rb | 2 +- .../projects/import_export/import_file_spec.rb | 5 +- 6 files changed, 7 insertions(+), 68 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index b141b5f472a..f8d4bcff55a 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -12,13 +12,7 @@ class Import::GitlabProjectsController < Import::BaseController end def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects - @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } end def jobs @@ -39,6 +33,8 @@ class Import::GitlabProjectsController < Import::BaseController tmp_file: File.expand_path(file.path), namespace_id: namespace_id, project_path: path) + + redirect_to status_import_gitlab_project_path end private diff --git a/app/views/import/gitlab_projects/status.html.haml b/app/views/import/gitlab_projects/status.html.haml index 2b879f0c595..e69de29bb2d 100644 --- a/app/views/import/gitlab_projects/status.html.haml +++ b/app/views/import/gitlab_projects/status.html.haml @@ -1,55 +0,0 @@ -- page_title "Gitlab_project import" -- header_title "Projects", root_path -%h3.page-title - %i.icon-gitlab.icon-gitlab-big - Import Gitlab projects - -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From Gitlab_project.org - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://gitlab_project.org/#{project.import_source}", target: "_blank" - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = link_to repo.full_name, "https://gitlab_project.org/#{repo.full_name}", target: "_blank" - %td.import-target - = repo.full_name - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -:javascript - new ImporterStatus("#{jobs_import_gitlab_project_path}", "#{import_gitlab_project_path}"); diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 9fc83afc4f7..5ff72f5ff8d 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -9,7 +9,7 @@ module Gitlab untar_with_options(archive: archive, dir: dir, options: 'zxf') end - def untar_czf(archive:, dir:) + def untar_xf(archive:, dir:) untar_with_options(archive: archive, dir: dir, options: 'xf') end diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 226499030af..227053481cd 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -15,8 +15,7 @@ module Gitlab def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, storage_path: storage_path) - restore_project_tree - restore_repo + project_tree.project if [restore_project_tree, restore_repo].all? end private diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 2c67bd5a845..994fc4bea5a 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -15,7 +15,7 @@ module Gitlab FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) - untar_zxf(archive: @path, dir: path_to_repo) + untar_xf(archive: @path, dir: path_to_repo) end private diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 32e131d7040..0bc2b85edb1 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -8,6 +8,8 @@ feature 'project import', feature: true, js: true do let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } background do + export_path = "#{Dir::tmpdir}/import_file_spec" + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) login_as(user) end @@ -27,9 +29,6 @@ feature 'project import', feature: true, js: true do sleep 1 click_on 'Continue to the next step' - - sleep 1 - end def drop_in_dropzone(file_path) -- cgit v1.2.1 From b5f2a7e139f0c1803afb68c26042ce4f8cec9148 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 2 May 2016 11:58:35 +0200 Subject: fixed integration test --- .../projects/import_export/import_file_spec.rb | 35 ++++++++-------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 0bc2b85edb1..05f6e289841 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -6,14 +6,19 @@ feature 'project import', feature: true, js: true do let(:user) { create(:admin) } let!(:namespace) { create(:namespace, name: "asd", owner: user) } let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } - + let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } background do - export_path = "#{Dir::tmpdir}/import_file_spec" allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) login_as(user) end + after(:each) do + FileUtils.rm_rf(export_path, secure: true) + end + scenario 'user imports an exported project successfully' do + expect(Project.all.count).to be_zero + visit new_project_path select2('2', from: '#project_namespace_id') @@ -22,29 +27,13 @@ feature 'project import', feature: true, js: true do expect(page).to have_content('GitLab export file') expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path') - attach_file('file', file) - - #TODO check timings - sleep 1 + attach_file('file', file) - click_on 'Continue to the next step' - end + click_on 'Continue to the next step' # import starts - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); - JS + expect(Project.last).not_to be_nil + expect(Project.last.issues).not_to be_empty + expect(Project.last.repo_exists?).to be true end end -- cgit v1.2.1 From 5a4f576359a71a57c70544568d9291cba53a2d39 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 2 May 2016 18:22:18 +0200 Subject: fixing issues with project members mapping. Also added some more JS magic to the import page --- app/views/projects/new.html.haml | 11 +++++++++++ lib/gitlab/import_export/import_export_reader.rb | 4 +++- lib/gitlab/import_export/project_tree_restorer.rb | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index b3d755b3790..f9d9216f626 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -131,3 +131,14 @@ var _href = $("a.import_gitlab_project").attr("href"); $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); }); + $('.import_gitlab_project').attr('disabled',true) + $('.import_gitlab_project').attr('title', 'Project path required.'); + $('#project_path').keyup(function(){ + if($(this).val().length !=0) { + $('.import_gitlab_project').attr('disabled', false); + $('.import_gitlab_project').attr('title',''); + } else { + $('.import_gitlab_project').attr('disabled',true); + $('.import_gitlab_project').attr('title', 'Project path required.'); + } + }) diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 717d3026f9e..4e46899ec7e 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -57,8 +57,10 @@ module Gitlab end def add_new_class(current_key, included_classes_hash, value) + only_except_hash = check_only_and_except(value) + # TODO: refactor this + value = (value.is_a?(Hash) ? value.merge(only_except_hash) : { value => only_except_hash }) if only_except_hash new_hash = { include: value } - new_hash.merge!(check_only_and_except(value)) included_classes_hash[current_key] = new_hash end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index d2834101758..0f2e3716779 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -49,6 +49,7 @@ module Gitlab project = Gitlab::ImportExport::ProjectFactory.create( project_params: project_params, user: @user) project.path = @project_path + project.name = @project_path project.save project end -- cgit v1.2.1 From 58b0b1a6615958d2ca7628a06898f81b316e6637 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 11:13:10 +0200 Subject: picking export stuff from the UI branch --- app/services/projects/import_export/export_service.rb | 3 ++- lib/gitlab/import_export/command_line_util.rb | 14 ++++++++++++++ lib/gitlab/import_export/import_export_reader.rb | 4 +++- lib/gitlab/import_export/project_tree_saver.rb | 1 + lib/gitlab/import_export/repo_bundler.rb | 1 + lib/gitlab/import_export/saver.rb | 1 + 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index ce13942c5d7..5d5573cba5c 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -1,8 +1,9 @@ module Projects module ImportExport class ExportService < BaseService + def execute(options = {}) - @shared = Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) + @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) save_project_tree bundle_repo save_all diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 7bf4b476b6c..5ff72f5ff8d 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -5,6 +5,14 @@ module Gitlab tar_with_options(archive: archive, dir: dir, options: 'cf') end + def untar_zxf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'zxf') + end + + def untar_xf(archive:, dir:) + untar_with_options(archive: archive, dir: dir, options: 'xf') + end + def tar_czf(archive:, dir:) tar_with_options(archive: archive, dir: dir, options: 'czf') end @@ -20,6 +28,12 @@ module Gitlab _output, status = Gitlab::Popen.popen(cmd) status.zero? end + + def untar_with_options(archive:, dir:, options:) + cmd = %W(tar -#{options} #{archive} -C #{dir}) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end end end end diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 717d3026f9e..4e46899ec7e 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -57,8 +57,10 @@ module Gitlab end def add_new_class(current_key, included_classes_hash, value) + only_except_hash = check_only_and_except(value) + # TODO: refactor this + value = (value.is_a?(Hash) ? value.merge(only_except_hash) : { value => only_except_hash }) if only_except_hash new_hash = { include: value } - new_hash.merge!(check_only_and_except(value)) included_classes_hash[current_key] = new_hash end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index f6fab0fe22d..394411a56c2 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -24,6 +24,7 @@ module Gitlab false end + # TODO remove magic keyword and move it to a shared config def project_filename "project.json" end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index 86c9501b708..e719ee2e9e3 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -26,6 +26,7 @@ module Gitlab false end + # TODO remove magic keyword and move it to a shared config def project_filename "project.bundle" end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index f87e0fdc7ea..634e58e6039 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -14,6 +14,7 @@ module Gitlab def save if compress_and_save remove_storage_path + Rails.logger.info("Saved project export #{archive_file}") archive_file else false -- cgit v1.2.1 From 9d306eb132bf153a0c93dd870d3a098028f12384 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 12:41:23 +0200 Subject: picking stuff from ui related to import --- app/models/project.rb | 10 +++++++++ app/workers/project_impor_worker.rb | 25 +++++++++++++++++++++++ lib/gitlab/import_export/import_service.rb | 25 ++++++++++++++--------- lib/gitlab/import_export/importer.rb | 3 ++- lib/gitlab/import_export/project_tree_restorer.rb | 15 +++++++------- lib/gitlab/import_export/repo_restorer.rb | 9 ++++---- 6 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 app/workers/project_impor_worker.rb diff --git a/app/models/project.rb b/app/models/project.rb index af62e8ecd90..3e782be637e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -359,6 +359,16 @@ class Project < ActiveRecord::Base def visible_to_user(user) where(id: user.authorized_projects.select(:id).reorder(nil)) end + + def create_from_import_job(current_user_id:, tmp_file:, namespace_id:, project_path:) + job_id = ProjectImportWorker.perform_async(current_user_id, tmp_file, namespace_id, project_path) + + if job_id + Rails.logger.info "Import job started for export #{tmp_file} with job ID #{job_id}" + else + Rails.logger.error "Import job failed to start for #{tmp_file}" + end + end end def team diff --git a/app/workers/project_impor_worker.rb b/app/workers/project_impor_worker.rb new file mode 100644 index 00000000000..0ef353fa441 --- /dev/null +++ b/app/workers/project_impor_worker.rb @@ -0,0 +1,25 @@ +class ProjectImportWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell, retry: false + + def perform(current_user_id, tmp_file, namespace_id, path) + current_user = User.find(current_user_id) + + project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file, + owner: current_user, + namespace_id: namespace_id, + project_path: path) + + # TODO: Move this to import service + # if result[:status] == :error + # project.update(import_error: result[:message]) + # project.import_fail + # return + # end + + project.repository.after_import + project.import_finish + end +end \ No newline at end of file diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 978a581f57a..227053481cd 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -3,18 +3,19 @@ module Gitlab class ImportService def self.execute(*args) - new(args).execute + new(*args).execute end - def initialize(options = {}) - @archive_file = options[:archive_file] - @current_user = options[:owner] + def initialize(archive_file:, owner:, namespace_id:, project_path:) + @archive_file = archive_file + @current_user = owner + @namespace_path = Namespace.find(namespace_id).path + @project_path = project_path end def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, storage_path: storage_path) - restore_project_tree - restore_repo(project_tree.project) + project_tree.project if [restore_project_tree, restore_repo].all? end private @@ -24,15 +25,19 @@ module Gitlab end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user) + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user, project_path: @project_path) end - def restore_repo(project) - Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project).restore + def restore_repo + Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project_tree.project).restore end def storage_path - @storage_path ||= Gitlab::ImportExport.export_path(relative_path: project.path_with_namespace) + @storage_path ||= Gitlab::ImportExport.export_path(relative_path: path_with_namespace) + end + + def path_with_namespace + File.join(@namespace_path, @project_path) end end end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 9f399845437..8f838287f97 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -13,13 +13,14 @@ module Gitlab end def import + FileUtils.mkdir_p(@storage_path) decompress_archive end private def decompress_archive - untar_czf(archive: @archive_file, dir: @storage_path) + untar_zxf(archive: @archive_file, dir: @storage_path) end end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 4e0f555afe9..0f2e3716779 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -1,11 +1,11 @@ module Gitlab module ImportExport class ProjectTreeRestorer - attr_reader :project - def initialize(path:, user:) + def initialize(path:, user:, project_path:) @path = File.join(path, 'project.json') @user = user + @project_path = project_path end def restore @@ -15,6 +15,10 @@ module Gitlab create_relations end + def project + @project ||= create_project + end + private def members_map @@ -40,14 +44,12 @@ module Gitlab Gitlab::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } end - def project - @project ||= create_project - end - def create_project project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project = Gitlab::ImportExport::ProjectFactory.create( project_params: project_params, user: @user) + project.path = @project_path + project.name = @project_path project.save project end @@ -63,7 +65,6 @@ module Gitlab end def process_sub_relation(relation_hash, relation_item, sub_relation) - sub_relation_object = nil if relation_hash.is_a?(Array) sub_relation_object = create_relation(sub_relation, relation_hash) else diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 47be303e22a..315ad88ee0c 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,18 +3,19 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project: , path:, bundler_file: ) + def initialize(project: , path: ) @project = project - @path = File.join(path, bundler_file) + # TODO remove magic keyword and move it to a shared config + @path = File.join(path, 'project.bundle') end def restore return false unless File.exists?(@path) - # Move repos dir to 'repositories.old' dir + # Move repos dir to 'repositories.old' dir FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) - untar_cf(archive: @path, dir: path_to_repo) + untar_xf(archive: @path, dir: path_to_repo) end private -- cgit v1.2.1 From 6139e1433c7f1925a7fc599106656990410a4184 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 12:46:00 +0200 Subject: fixing some rubucop warnings --- .../import/gitlab_project_controller.rb | 3 +-- app/workers/project_impor_worker.rb | 25 ---------------------- app/workers/project_import_worker.rb | 25 ++++++++++++++++++++++ 3 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 app/workers/project_impor_worker.rb create mode 100644 app/workers/project_import_worker.rb diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb index fa3e0c7876d..ab0da196ac1 100644 --- a/app/controllers/import/gitlab_project_controller.rb +++ b/app/controllers/import/gitlab_project_controller.rb @@ -28,12 +28,11 @@ class Import::GitlabProjectController < Import::BaseController def create @file = params[:file] - # @project_name = repo_owner = current_user.username @target_namespace = params[:new_namespace].presence || repo_owner - namespace = get_or_create_namespace || (render and return) + # namespace = get_or_create_namespace || (render and return) @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) end diff --git a/app/workers/project_impor_worker.rb b/app/workers/project_impor_worker.rb deleted file mode 100644 index 0ef353fa441..00000000000 --- a/app/workers/project_impor_worker.rb +++ /dev/null @@ -1,25 +0,0 @@ -class ProjectImportWorker - include Sidekiq::Worker - include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell, retry: false - - def perform(current_user_id, tmp_file, namespace_id, path) - current_user = User.find(current_user_id) - - project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file, - owner: current_user, - namespace_id: namespace_id, - project_path: path) - - # TODO: Move this to import service - # if result[:status] == :error - # project.update(import_error: result[:message]) - # project.import_fail - # return - # end - - project.repository.after_import - project.import_finish - end -end \ No newline at end of file diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb new file mode 100644 index 00000000000..0e8b9552442 --- /dev/null +++ b/app/workers/project_import_worker.rb @@ -0,0 +1,25 @@ +class ProjectImportWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell, retry: false + + def perform(current_user_id, tmp_file, namespace_id, path) + current_user = User.find(current_user_id) + + project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file, + owner: current_user, + namespace_id: namespace_id, + project_path: path) + + # TODO: Move this to import service + # if result[:status] == :error + # project.update(import_error: result[:message]) + # project.import_fail + # return + # end + + project.repository.after_import + project.import_finish + end +end -- cgit v1.2.1 From 1990616a21a38e8d0496c066e753282d9a061162 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 13:06:00 +0200 Subject: fixed warning --- spec/lib/gitlab/import_export/project_tree_saver_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 8a923dde467..5c1b9af2c13 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do labels: [label], snippets: [snippet], releases: [release] - ) + ) end let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) } let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } -- cgit v1.2.1 From 315195fe56743db7f0546d3705b4c82fb3462d63 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 13:18:39 +0200 Subject: fix schema file --- db/schema.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 40a295ca635..04aee737e4c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -78,8 +78,8 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.text "shared_runners_text" t.integer "metrics_packet_size", default: 1 + t.text "shared_runners_text" end create_table "audit_events", force: :cascade do |t| @@ -426,9 +426,9 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.string "state" t.integer "iid" t.integer "updated_by_id" + t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" - t.integer "moved_to_id" t.date "due_date" end -- cgit v1.2.1 From fc329696e269044a9b366dbd5135aedca93bccd4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 13:40:49 +0200 Subject: typo --- app/views/import/gitlab_projects/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 1158a04ea46..d01f0190f3f 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -2,7 +2,7 @@ - header_title "Projects", root_path %h3.page-title %i.fa.fa-bug - Import projects from FogBugz + Import projects from Gitlab %hr = form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do -- cgit v1.2.1 From b69c8c280c029781e87834610a89704210f1a5c4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 15:07:05 +0200 Subject: fixing some export issues in spec --- .../import_export/test_project_export.tar.gz | Bin 353039 -> 352529 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index bc447b4a62a..0c20898086d 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 17cfce7adcee712f7e06d8ef0badf926f8eac105 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 3 May 2016 15:09:28 +0200 Subject: typo --- app/views/import/gitlab_projects/new.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index d01f0190f3f..e8e1c02d441 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -1,8 +1,8 @@ -- page_title "FogBugz Import" +- page_title "GitLab Import" - header_title "Projects", root_path %h3.page-title %i.fa.fa-bug - Import projects from Gitlab + Import projects from GitLab %hr = form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do -- cgit v1.2.1 From bd2ebf37e6c5aa07f8d29f4ac8c88fc8333a2297 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 10:57:09 +0200 Subject: use worker in controller --- app/controllers/projects_controller.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 62f8b376c18..f15f20dcf52 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -191,8 +191,7 @@ class ProjectsController < Projects::ApplicationController end def export - #TODO: Move to worker - ::Projects::ImportExport::ExportService.new(@project, current_user).execute + @project.add_export_job(current_user_id: current_user.id) redirect_to( edit_project_path(@project), @@ -201,7 +200,11 @@ class ProjectsController < Projects::ApplicationController end def download_export - send_file export_project_path, disposition: 'attachment' + if export_project_path + send_file export_project_path, disposition: 'attachment' + else + render_404 + end end def toggle_star -- cgit v1.2.1 From bc8eebf04a32d893d5b536deb2e7ffc205d9fcda Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 10:59:33 +0200 Subject: add export worker to process project export async --- app/models/project.rb | 10 ++++++++++ app/workers/project_export_worker.rb | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 app/workers/project_export_worker.rb diff --git a/app/models/project.rb b/app/models/project.rb index af62e8ecd90..8781a3cf7a4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1034,4 +1034,14 @@ class Project < ActiveRecord::Base def wiki @wiki ||= ProjectWiki.new(self, self.owner) end + + def add_export_job(current_user_id:) + job_id = ProjectExportWorker.perform_async(current_user_id, self.id) + + if job_id + Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}" + else + Rails.logger.error "Export job failed to start for project ID #{self.id}" + end + end end diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb new file mode 100644 index 00000000000..a1add5395b7 --- /dev/null +++ b/app/workers/project_export_worker.rb @@ -0,0 +1,13 @@ +class ProjectExportWorker + include Sidekiq::Worker + + # TODO: enabled retry - disabled for QA purposes + sidekiq_options queue: :gitlab_shell, retry: false + + def perform(current_user_id, project_id) + current_user = User.find(current_user_id) + project = Project.find(project_id) + ::Projects::ImportExport::ExportService.new(project, current_user).execute + # TODO : Handle errors + end +end -- cgit v1.2.1 From 9b21207305f7516b132211b5ead584beb37b8948 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 11:01:55 +0200 Subject: updated import worker --- app/workers/project_import_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb index 0e8b9552442..aaa59f79d7f 100644 --- a/app/workers/project_import_worker.rb +++ b/app/workers/project_import_worker.rb @@ -1,7 +1,7 @@ class ProjectImportWorker include Sidekiq::Worker - include Gitlab::ShellAdapter + # TODO: enabled retry - disabled for QA purposes sidekiq_options queue: :gitlab_shell, retry: false def perform(current_user_id, tmp_file, namespace_id, path) -- cgit v1.2.1 From e5ee7e8692b8c9530cef61f3b4ce14f8bf278452 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 11:10:25 +0200 Subject: typos in export page --- app/views/import/gitlab_projects/new.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index e8e1c02d441..d93ae42c1bf 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -1,18 +1,18 @@ - page_title "GitLab Import" - header_title "Projects", root_path %h3.page-title - %i.fa.fa-bug + %i.fa.fa-gitlab Import projects from GitLab %hr = form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do %p - To get started you add your project export file below. + To get started add your exported project file below: .form-group = hidden_field_tag :namespace_id, @namespace_id = hidden_field_tag :path, @path = label_tag :file, class: 'control-label' do - %span GitLab export file + %span GitLab project export .col-sm-10 = file_field_tag :file, class: '' -- cgit v1.2.1 From 14dd2d4ad0be905385ec524a3022b9b81cb4671b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 15:05:28 +0200 Subject: fixed import file spec --- spec/features/projects/import_export/import_file_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 05f6e289841..b9173e44554 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -25,7 +25,7 @@ feature 'project import', feature: true, js: true do fill_in :project_path, with:'test-project-path', visible: true click_link 'GitLab project' - expect(page).to have_content('GitLab export file') + expect(page).to have_content('GitLab project export') expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path') attach_file('file', file) -- cgit v1.2.1 From 773c39cca3cc6c4f9edd9387e99d3a85924ac5af Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 15:13:44 +0200 Subject: fixed import export reader spec --- lib/gitlab/import_export/import_export_reader.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 4e46899ec7e..14049cb1bd2 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -41,7 +41,7 @@ module Gitlab current_key = hash.keys.first value = process_current_class(hash, included_classes_hash, value) if included_classes_hash[current_key] - add_class(current_key, included_classes_hash, value) + add_to_class(current_key, included_classes_hash, value) else add_new_class(current_key, included_classes_hash, value) end @@ -58,13 +58,18 @@ module Gitlab def add_new_class(current_key, included_classes_hash, value) only_except_hash = check_only_and_except(value) - # TODO: refactor this - value = (value.is_a?(Hash) ? value.merge(only_except_hash) : { value => only_except_hash }) if only_except_hash - new_hash = { include: value } - included_classes_hash[current_key] = new_hash + parsed_hash = { include: value } + unless only_except_hash.empty? + if value.is_a?(Hash) + parsed_hash = { include: value.merge(only_except_hash) } + else + parsed_hash = { include: { value => only_except_hash } } + end + end + included_classes_hash[current_key] = parsed_hash end - def add_class(current_key, included_classes_hash, value) + def add_to_class(current_key, included_classes_hash, value) only_except_hash = check_only_and_except(value) value = { value => only_except_hash } unless only_except_hash.empty? old_values = included_classes_hash[current_key][:include] -- cgit v1.2.1 From a4d242b887f214493e73e2d44110974a417f3418 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 17:42:02 +0200 Subject: refactored some namespace stuff and fixed project tree restorer spec. also removing controller so that it belongs to the UI MR --- .../import/gitlab_project_controller.rb | 45 --- fixtures/import_export/project.json | 434 --------------------- lib/gitlab/import_export/import_service.rb | 6 +- lib/gitlab/import_export/project.json | 434 +++++++++++++++++++++ lib/gitlab/import_export/project_factory.rb | 9 +- lib/gitlab/import_export/project_tree_restorer.rb | 10 +- lib/gitlab/import_export/repo_restorer.rb | 2 +- .../import_export/project_tree_restorer_spec.rb | 3 +- spec/spec_helper.rb | 4 + 9 files changed, 454 insertions(+), 493 deletions(-) delete mode 100644 app/controllers/import/gitlab_project_controller.rb delete mode 100644 fixtures/import_export/project.json create mode 100644 lib/gitlab/import_export/project.json diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb deleted file mode 100644 index ab0da196ac1..00000000000 --- a/app/controllers/import/gitlab_project_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Import::GitlabProjectController < Import::BaseController - before_action :verify_gitlab_project_import_enabled - before_action :gitlab_project_auth, except: :callback - - rescue_from OAuth::Error, with: :gitlab_project_unauthorized - - #TODO permissions stuff - - def callback - - redirect_to status_import_gitlab_project_url - end - - def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects - - @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } - end - - def jobs - jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) - render json: jobs - end - - def create - @file = params[:file] - - repo_owner = current_user.username - @target_namespace = params[:new_namespace].presence || repo_owner - - # namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) - end - - private - - def verify_gitlab_project_import_enabled - render_404 unless gitlab_project_import_enabled? - end -end diff --git a/fixtures/import_export/project.json b/fixtures/import_export/project.json deleted file mode 100644 index 4a12c951dcb..00000000000 --- a/fixtures/import_export/project.json +++ /dev/null @@ -1,434 +0,0 @@ -{ - "name": "searchable_project", - "path": "gitlabhq", - "description": null, - "issues_enabled": true, - "wall_enabled": false, - "merge_requests_enabled": true, - "wiki_enabled": true, - "snippets_enabled": true, - "visibility_level": 20, - "archived": false, - "issues": [ - { - "id": 1, - "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", - "assignee_id": 1, - "author_id": 2, - "project_id": 6, - "created_at": "2016-04-13T14:40:32.471Z", - "updated_at": "2016-04-13T14:40:38.956Z", - "position": 0, - "branch_name": null, - "description": null, - "milestone_id": null, - "state": "opened", - "iid": 1, - "updated_by_id": null, - "confidential": false, - "deleted_at": null, - "moved_to_id": null, - "notes": [ - { - "id": 1, - "note": ":+1: issue", - "noteable_type": "Issue", - "author_id": 21, - "created_at": "2016-04-13T14:40:38.944Z", - "updated_at": "2016-04-13T14:40:38.944Z", - "project_id": 8, - "attachment": { - "url": null - }, - "line_code": null, - "commit_id": null, - "noteable_id": 1, - "system": false, - "st_diff": null, - "updated_by_id": null, - "is_award": false - } - ] - } - ], - "labels": [ - { - "id": 1, - "title": "label1", - "color": "#990000", - "project_id": 6, - "created_at": "2016-04-13T14:40:34.704Z", - "updated_at": "2016-04-13T14:40:36.891Z", - "template": false, - "description": null - } - ], - "milestones": [ - { - "id": 1, - "title": "Milestone v1.2", - "project_id": 6, - "description": null, - "due_date": null, - "created_at": "2016-04-13T14:40:37.901Z", - "updated_at": "2016-04-13T14:40:37.901Z", - "state": "active", - "iid": 1 - } - ], - "snippets": [ - { - "id": 1, - "title": "Illo ipsa maxime magni aut.", - "content": "Excepturi delectus ut harum est molestiae dolor.", - "author_id": 10, - "project_id": 6, - "created_at": "2016-04-13T14:40:35.603Z", - "updated_at": "2016-04-13T14:40:36.903Z", - "file_name": "daphne.mraz", - "visibility_level": 0 - } - ], - "releases": [ - { - "id": 1, - "tag": "v1.1.0", - "description": "Awesome release", - "project_id": 6, - "created_at": "2016-04-13T14:40:36.223Z", - "updated_at": "2016-04-13T14:40:36.913Z" - } - ], - "events": [ - { - "id": 1, - "target_type": null, - "target_id": null, - "title": null, - "data": null, - "project_id": 6, - "created_at": "2016-04-13T14:40:40.122Z", - "updated_at": "2016-04-13T14:40:40.122Z", - "action": 8, - "author_id": 1 - } - ], - "project_members": [ - { - "id": 1, - "user": { - "id": 1, - "email": "norval.gulgowski@schambergerboyle.co.uk", - "created_at": "2016-04-13T14:40:30.963Z", - "updated_at": "2016-04-13T14:40:30.963Z", - "name": "Jalon Cormier DVM", - "admin": false, - "projects_limit": 42, - "skype": "", - "linkedin": "", - "twitter": "", - "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", - "theme_id": 2, - "bio": null, - "username": "vance.turner1", - "can_create_group": true, - "can_create_team": false, - "state": "active", - "color_scheme_id": 1, - "notification_level": 1, - "password_expires_at": null, - "created_by_id": null, - "last_credential_check_at": null, - "avatar": { - "url": null - }, - "hide_no_ssh_key": false, - "website_url": "", - "notification_email": "norval.gulgowski@schambergerboyle.co.uk", - "hide_no_password": false, - "password_automatically_set": false, - "location": null, - "encrypted_otp_secret": null, - "encrypted_otp_secret_iv": null, - "encrypted_otp_secret_salt": null, - "otp_required_for_login": false, - "otp_backup_codes": null, - "public_email": "", - "dashboard": "projects", - "project_view": "readme", - "consumed_timestep": null, - "layout": "fixed", - "hide_project_limit": false, - "otp_grace_period_started_at": null, - "ldap_email": false, - "external": false - } - } - ], - "merge_requests": [ - { - "id": 1, - "target_branch": "feature", - "source_branch": "master", - "source_project_id": 2, - "author_id": 5, - "assignee_id": null, - "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", - "created_at": "2016-04-13T14:40:33.381Z", - "updated_at": "2016-04-13T14:40:39.850Z", - "milestone_id": null, - "state": "opened", - "merge_status": "can_be_merged", - "target_project_id": 6, - "iid": 1, - "description": null, - "position": 0, - "locked_at": null, - "updated_by_id": null, - "merge_error": null, - "merge_params": { - }, - "merge_when_build_succeeds": false, - "merge_user_id": null, - "merge_commit_sha": null, - "deleted_at": null, - "merge_request_diff": { - "id": 1, - "state": "collected", - "st_commits": [ - { - "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" - ], - "authored_date": "2014-02-27T10:01:38.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T10:01:38.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - ], - "authored_date": "2014-02-27T09:57:31.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:57:31.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "d14d6c0abdd253381df51a723d58691b2ee1ab08" - ], - "authored_date": "2014-02-27T09:54:21.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:54:21.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" - ], - "authored_date": "2014-02-27T09:49:50.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:49:50.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" - ], - "authored_date": "2014-02-27T09:48:32.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:48:32.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - } - ], - "st_diffs": [ - { - "diff": "Binary files a/.DS_Store and /dev/null differ\n", - "new_path": ".DS_Store", - "old_path": ".DS_Store", - "a_mode": "100644", - "b_mode": "0", - "new_file": false, - "renamed_file": false, - "deleted_file": true, - "too_large": false - }, - { - "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", - "new_path": ".gitignore", - "old_path": ".gitignore", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", - "new_path": ".gitmodules", - "old_path": ".gitmodules", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", - "new_path": "files/.DS_Store", - "old_path": "files/.DS_Store", - "a_mode": "100644", - "b_mode": "0", - "new_file": false, - "renamed_file": false, - "deleted_file": true, - "too_large": false - }, - { - "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", - "new_path": "files/ruby/popen.rb", - "old_path": "files/ruby/popen.rb", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", - "new_path": "files/ruby/regex.rb", - "old_path": "files/ruby/regex.rb", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", - "new_path": "gitlab-grack", - "old_path": "gitlab-grack", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", - "new_path": "gitlab-shell", - "old_path": "gitlab-shell", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false, - "too_large": false - } - ], - "merge_request_id": 1, - "created_at": "2016-04-13T14:40:33.474Z", - "updated_at": "2016-04-13T14:40:33.834Z", - "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", - "real_size": "8" - }, - "notes": [ - { - "id": 2, - "note": ":+1: merge_request", - "noteable_type": "MergeRequest", - "author_id": 24, - "created_at": "2016-04-13T14:40:39.832Z", - "updated_at": "2016-04-13T14:40:39.832Z", - "project_id": 9, - "attachment": { - "url": null - }, - "line_code": null, - "commit_id": null, - "noteable_id": 1, - "system": false, - "st_diff": null, - "updated_by_id": null, - "is_award": false - } - ] - } - ], - "ci_commits": [ - { - "id": 2, - "project_id": 6, - "ref": "master", - "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "before_sha": null, - "push_data": null, - "created_at": "2016-04-13T14:40:37.759Z", - "updated_at": "2016-04-13T14:40:37.759Z", - "tag": false, - "yaml_errors": null, - "committed_at": null, - "gl_project_id": 6, - "statuses": [ - { - "id": 1, - "project_id": null, - "status": "success", - "finished_at": "2016-01-26T07:23:42.000Z", - "trace": null, - "created_at": "2016-04-13T14:40:37.717Z", - "updated_at": "2016-04-13T14:40:37.771Z", - "started_at": "2016-01-26T07:21:42.000Z", - "runner_id": null, - "coverage": null, - "commit_id": 2, - "commands": null, - "job_id": null, - "name": "default", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": null, - "trigger_request_id": null, - "stage_idx": null, - "tag": null, - "ref": null, - "user_id": null, - "target_url": null, - "description": "commit status", - "artifacts_file": null, - "gl_project_id": 7, - "artifacts_metadata": null, - "erased_by_id": null, - "erased_at": null - } - ] - } - ] -} \ No newline at end of file diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 227053481cd..97ee8a7fc90 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -9,7 +9,7 @@ module Gitlab def initialize(archive_file:, owner:, namespace_id:, project_path:) @archive_file = archive_file @current_user = owner - @namespace_path = Namespace.find(namespace_id).path + @namespace = Namespace.find(namespace_id) @project_path = project_path end @@ -25,7 +25,7 @@ module Gitlab end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user, project_path: @project_path) + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user, project_path: @project_path, namespace_id: @namespace.id) end def restore_repo @@ -37,7 +37,7 @@ module Gitlab end def path_with_namespace - File.join(@namespace_path, @project_path) + File.join(@namespace.path, @project_path) end end end diff --git a/lib/gitlab/import_export/project.json b/lib/gitlab/import_export/project.json new file mode 100644 index 00000000000..4a12c951dcb --- /dev/null +++ b/lib/gitlab/import_export/project.json @@ -0,0 +1,434 @@ +{ + "name": "searchable_project", + "path": "gitlabhq", + "description": null, + "issues_enabled": true, + "wall_enabled": false, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "visibility_level": 20, + "archived": false, + "issues": [ + { + "id": 1, + "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", + "assignee_id": 1, + "author_id": 2, + "project_id": 6, + "created_at": "2016-04-13T14:40:32.471Z", + "updated_at": "2016-04-13T14:40:38.956Z", + "position": 0, + "branch_name": null, + "description": null, + "milestone_id": null, + "state": "opened", + "iid": 1, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "notes": [ + { + "id": 1, + "note": ":+1: issue", + "noteable_type": "Issue", + "author_id": 21, + "created_at": "2016-04-13T14:40:38.944Z", + "updated_at": "2016-04-13T14:40:38.944Z", + "project_id": 8, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "labels": [ + { + "id": 1, + "title": "label1", + "color": "#990000", + "project_id": 6, + "created_at": "2016-04-13T14:40:34.704Z", + "updated_at": "2016-04-13T14:40:36.891Z", + "template": false, + "description": null + } + ], + "milestones": [ + { + "id": 1, + "title": "Milestone v1.2", + "project_id": 6, + "description": null, + "due_date": null, + "created_at": "2016-04-13T14:40:37.901Z", + "updated_at": "2016-04-13T14:40:37.901Z", + "state": "active", + "iid": 1 + } + ], + "snippets": [ + { + "id": 1, + "title": "Illo ipsa maxime magni aut.", + "content": "Excepturi delectus ut harum est molestiae dolor.", + "author_id": 10, + "project_id": 6, + "created_at": "2016-04-13T14:40:35.603Z", + "updated_at": "2016-04-13T14:40:36.903Z", + "file_name": "daphne.mraz", + "visibility_level": 0 + } + ], + "releases": [ + { + "id": 1, + "tag": "v1.1.0", + "description": "Awesome release", + "project_id": 6, + "created_at": "2016-04-13T14:40:36.223Z", + "updated_at": "2016-04-13T14:40:36.913Z" + } + ], + "events": [ + { + "id": 1, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 6, + "created_at": "2016-04-13T14:40:40.122Z", + "updated_at": "2016-04-13T14:40:40.122Z", + "action": 8, + "author_id": 1 + } + ], + "project_members": [ + { + "id": 1, + "user": { + "id": 1, + "email": "norval.gulgowski@schambergerboyle.co.uk", + "created_at": "2016-04-13T14:40:30.963Z", + "updated_at": "2016-04-13T14:40:30.963Z", + "name": "Jalon Cormier DVM", + "admin": false, + "projects_limit": 42, + "skype": "", + "linkedin": "", + "twitter": "", + "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", + "theme_id": 2, + "bio": null, + "username": "vance.turner1", + "can_create_group": true, + "can_create_team": false, + "state": "active", + "color_scheme_id": 1, + "notification_level": 1, + "password_expires_at": null, + "created_by_id": null, + "last_credential_check_at": null, + "avatar": { + "url": null + }, + "hide_no_ssh_key": false, + "website_url": "", + "notification_email": "norval.gulgowski@schambergerboyle.co.uk", + "hide_no_password": false, + "password_automatically_set": false, + "location": null, + "encrypted_otp_secret": null, + "encrypted_otp_secret_iv": null, + "encrypted_otp_secret_salt": null, + "otp_required_for_login": false, + "otp_backup_codes": null, + "public_email": "", + "dashboard": "projects", + "project_view": "readme", + "consumed_timestep": null, + "layout": "fixed", + "hide_project_limit": false, + "otp_grace_period_started_at": null, + "ldap_email": false, + "external": false + } + } + ], + "merge_requests": [ + { + "id": 1, + "target_branch": "feature", + "source_branch": "master", + "source_project_id": 2, + "author_id": 5, + "assignee_id": null, + "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", + "created_at": "2016-04-13T14:40:33.381Z", + "updated_at": "2016-04-13T14:40:39.850Z", + "milestone_id": null, + "state": "opened", + "merge_status": "can_be_merged", + "target_project_id": 6, + "iid": 1, + "description": null, + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "merge_request_diff": { + "id": 1, + "state": "collected", + "st_commits": [ + { + "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + ], + "authored_date": "2014-02-27T10:01:38.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T10:01:38.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + ], + "authored_date": "2014-02-27T09:57:31.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:57:31.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "d14d6c0abdd253381df51a723d58691b2ee1ab08" + ], + "authored_date": "2014-02-27T09:54:21.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:54:21.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" + ], + "authored_date": "2014-02-27T09:49:50.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:49:50.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:48:32.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:48:32.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "new_path": ".DS_Store", + "old_path": ".DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "new_path": ".gitignore", + "old_path": ".gitignore", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "new_path": ".gitmodules", + "old_path": ".gitmodules", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "new_path": "files/.DS_Store", + "old_path": "files/.DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", + "new_path": "files/ruby/popen.rb", + "old_path": "files/ruby/popen.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "new_path": "files/ruby/regex.rb", + "old_path": "files/ruby/regex.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "new_path": "gitlab-grack", + "old_path": "gitlab-grack", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "new_path": "gitlab-shell", + "old_path": "gitlab-shell", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 1, + "created_at": "2016-04-13T14:40:33.474Z", + "updated_at": "2016-04-13T14:40:33.834Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "8" + }, + "notes": [ + { + "id": 2, + "note": ":+1: merge_request", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-04-13T14:40:39.832Z", + "updated_at": "2016-04-13T14:40:39.832Z", + "project_id": 9, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "ci_commits": [ + { + "id": 2, + "project_id": 6, + "ref": "master", + "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "before_sha": null, + "push_data": null, + "created_at": "2016-04-13T14:40:37.759Z", + "updated_at": "2016-04-13T14:40:37.759Z", + "tag": false, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 6, + "statuses": [ + { + "id": 1, + "project_id": null, + "status": "success", + "finished_at": "2016-01-26T07:23:42.000Z", + "trace": null, + "created_at": "2016-04-13T14:40:37.717Z", + "updated_at": "2016-04-13T14:40:37.771Z", + "started_at": "2016-01-26T07:21:42.000Z", + "runner_id": null, + "coverage": null, + "commit_id": 2, + "commands": null, + "job_id": null, + "name": "default", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": null, + "trigger_request_id": null, + "stage_idx": null, + "tag": null, + "ref": null, + "user_id": null, + "target_url": null, + "description": "commit status", + "artifacts_file": null, + "gl_project_id": 7, + "artifacts_metadata": null, + "erased_by_id": null, + "erased_at": null + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/gitlab/import_export/project_factory.rb b/lib/gitlab/import_export/project_factory.rb index c7137844a0a..6cd4736649b 100644 --- a/lib/gitlab/import_export/project_factory.rb +++ b/lib/gitlab/import_export/project_factory.rb @@ -3,17 +3,19 @@ module Gitlab module ProjectFactory extend self - def create(project_params:, user:) + def create(project_params:, user:, namespace_id:) project = Project.new(project_params.except('id')) project.creator = user - check_namespace(project_params['namespace_id'], project, user) + check_namespace(namespace_id, project, user) end def check_namespace(namespace_id, project, user) if namespace_id # Find matching namespace and check if it allowed # for current user if namespace_id passed. - unless allowed_namespace?(user, namespace_id) + if allowed_namespace?(user, namespace_id) + project.namespace_id = namespace_id + else project.namespace_id = nil deny_namespace(project) end @@ -34,7 +36,6 @@ module Gitlab def deny_namespace(project) project.errors.add(:namespace, "is not valid") end - end end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 0f2e3716779..0bc23dfaa24 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -2,10 +2,11 @@ module Gitlab module ImportExport class ProjectTreeRestorer - def initialize(path:, user:, project_path:) + def initialize(path:, user:, project_path:, namespace_id:) @path = File.join(path, 'project.json') @user = user @project_path = project_path + @namespace_id = namespace_id end def restore @@ -30,9 +31,8 @@ module Gitlab saved = [] relation_list.each do |relation| next if !relation.is_a?(Hash) && tree_hash[relation.to_s].blank? - if relation.is_a?(Hash) - create_sub_relations(relation, tree_hash) - end + create_sub_relations(relation, tree_hash) if relation.is_a?(Hash) + relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) saved << project.update_attribute(relation_key, relation_hash) @@ -47,7 +47,7 @@ module Gitlab def create_project project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project = Gitlab::ImportExport::ProjectFactory.create( - project_params: project_params, user: @user) + project_params: project_params, user: @user, namespace_id: @namespace_id) project.path = @project_path project.name = @project_path project.save diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 315ad88ee0c..b34581deeb5 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,7 +3,7 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project: , path: ) + def initialize(project:, path:) @project = project # TODO remove magic keyword and move it to a shared config @path = File.join(path, 'project.bundle') diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index f3d3a57ddd7..aa8a12c4caa 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -4,7 +4,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe :restore do let(:user) { create(:user) } - let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(path: "fixtures/import_export/project.json", user: user) } + let(:namespace) { create(:namespace, owner: user) } + let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(path: "lib/gitlab/import_export/", user: user, project_path: 'project', namespace_id: namespace.id) } context 'JSON' do let(:restored_project_json) do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 596d607f2a1..b48e54b78fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -58,3 +58,7 @@ RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do end ActiveRecord::Migration.maintain_test_schema! + +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, timeout: 1.minute) +end \ No newline at end of file -- cgit v1.2.1 From 48fcf54040bf049d7f7d959b1033ad28b3966770 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 4 May 2016 18:01:41 +0200 Subject: add gitlab project controller - still WIP --- .../import/gitlab_project_controller.rb | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 app/controllers/import/gitlab_project_controller.rb diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb new file mode 100644 index 00000000000..ab0da196ac1 --- /dev/null +++ b/app/controllers/import/gitlab_project_controller.rb @@ -0,0 +1,45 @@ +class Import::GitlabProjectController < Import::BaseController + before_action :verify_gitlab_project_import_enabled + before_action :gitlab_project_auth, except: :callback + + rescue_from OAuth::Error, with: :gitlab_project_unauthorized + + #TODO permissions stuff + + def callback + + redirect_to status_import_gitlab_project_url + end + + def status + @repos = client.projects + @incompatible_repos = client.incompatible_projects + + @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @file = params[:file] + + repo_owner = current_user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + # namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) + end + + private + + def verify_gitlab_project_import_enabled + render_404 unless gitlab_project_import_enabled? + end +end -- cgit v1.2.1 From c1858da9e154d4ac0b18dc93ec13464700c1f11b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 09:35:27 +0200 Subject: remove spec helper conf --- spec/spec_helper.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b48e54b78fb..596d607f2a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -58,7 +58,3 @@ RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do end ActiveRecord::Migration.maintain_test_schema! - -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, timeout: 1.minute) -end \ No newline at end of file -- cgit v1.2.1 From a9ac9ed105a710194099a4e07a2e87712d59a59a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 10:05:42 +0200 Subject: fix import feature --- features/steps/dashboard/new_project.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index a0aad66184d..ad20201c2c5 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -19,7 +19,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps expect(page).to have_link('GitLab.com') expect(page).to have_link('Gitorious.org') expect(page).to have_link('Google Code') - expect(page).to have_link('Any repo by URL') + expect(page).to have_link('Repo by URL') + expect(page).to have_link('GitLab project') end step 'I click on "Import project from GitHub"' do @@ -36,7 +37,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps end end - step 'I click on "Any repo by URL"' do + step 'I click on "Repo by URL"' do first('.import_git').click end -- cgit v1.2.1 From e14d1051d2c04bad514d840e1eff35ce24b05922 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 11:02:22 +0200 Subject: fix import feature v2 --- features/dashboard/new_project.feature | 2 +- lib/gitlab/import_sources.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature index 76392068357..56b4a639c01 100644 --- a/features/dashboard/new_project.feature +++ b/features/dashboard/new_project.feature @@ -14,7 +14,7 @@ Background: @javascript Scenario: I should see instructions on how to import from Git URL Given I see "New Project" page - When I click on "Any repo by URL" + When I click on "Repo by URL" Then I see instructions on how to import from Git URL @javascript diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 2b5658a8b64..4cae819d356 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -20,7 +20,7 @@ module Gitlab 'Gitorious.org' => 'gitorious', 'Google Code' => 'google_code', 'FogBugz' => 'fogbugz', - 'Any repo by URL' => 'git', + 'Repo by URL' => 'git', 'GitLab project' => 'gitlab_project' } end -- cgit v1.2.1 From 6612ca096a496caaa0d30724190964df8aac2f46 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 12:01:18 +0200 Subject: update repo and wiki repo bundler to use git bundle instead of compressing via tar --- app/services/projects/import_export/export_service.rb | 6 ++++++ lib/gitlab/import_export/repo_bundler.rb | 3 +-- lib/gitlab/import_export/wiki_repo_bundler.rb | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 5d5573cba5c..47ceff18af6 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -4,8 +4,10 @@ module Projects def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) + # TODO handle errors save_project_tree bundle_repo + bundle_wiki_repo save_all end @@ -19,6 +21,10 @@ module Projects Gitlab::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle end + def bundle_wiki_repo + Gitlab::ImportExport::WikiRepoBundler.new(project: project, shared: @shared).bundle + end + def save_all Gitlab::ImportExport::Saver.save(storage_path: @shared.export_path) end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index e719ee2e9e3..cd871687d76 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -20,9 +20,8 @@ module Gitlab def bundle_to_disk FileUtils.mkdir_p(@export_path) - tar_cf(archive: full_path, dir: path_to_repo) + git_bundle(repo_path: path_to_repo, bundle_path: @full_path) rescue - #TODO: handle error false end diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb index 7821c671628..17f1530d5a0 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -3,7 +3,7 @@ module Gitlab class WikiRepoBundler < RepoBundler def bundle @wiki = ProjectWiki.new(@project) - return false if !wiki? + return true if !wiki? # it's okay to have no Wiki @full_path = File.join(@export_path, project_filename) bundle_to_disk end @@ -12,7 +12,6 @@ module Gitlab FileUtils.mkdir_p(@export_path) git_bundle(repo_path: path_to_repo, bundle_path: @full_path) rescue - #TODO: handle error false end -- cgit v1.2.1 From 92f4bde427652a33ccb42e93de1cc781289a6c8a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 12:39:18 +0200 Subject: use git bundle in import and add wiki repo to import --- lib/gitlab/import_export/command_line_util.rb | 6 ++++++ lib/gitlab/import_export/import_service.rb | 16 ++++++++++++++-- lib/gitlab/import_export/repo_restorer.rb | 11 +++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 5ff72f5ff8d..f32e2715997 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -23,6 +23,12 @@ module Gitlab status.zero? end + def git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + cmd = %W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}) + _output, status = Gitlab::Popen.popen(cmd) + status.zero? + end + def tar_with_options(archive:, dir:, options:) cmd = %W(tar -#{options} #{archive} -C #{dir} .) _output, status = Gitlab::Popen.popen(cmd) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 97ee8a7fc90..6878f4bbc8c 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -15,7 +15,7 @@ module Gitlab def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, storage_path: storage_path) - project_tree.project if [restore_project_tree, restore_repo].all? + project_tree.project if [restore_project_tree, restore_repo, restore_wiki_repo].all? end private @@ -29,7 +29,11 @@ module Gitlab end def restore_repo - Gitlab::ImportExport::RepoRestorer.new(path: storage_path, project: project_tree.project).restore + Gitlab::ImportExport::RepoRestorer.new(path: repo_path, project: project_tree.project).restore + end + + def restore_wiki_repo + Gitlab::ImportExport::RepoRestorer.new(path: wiki_repo_path, project: project_tree.project).restore end def storage_path @@ -39,6 +43,14 @@ module Gitlab def path_with_namespace File.join(@namespace.path, @project_path) end + + def repo_path + File.join('storage_path', 'project.bundle') + end + + def wiki_repo_path + File.join('storage_path', 'project.wiki.bundle') + end end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index b34581deeb5..dc45238bf2a 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,19 +3,18 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, path:) + def initialize(project:, path_to_bundle: ) @project = project - # TODO remove magic keyword and move it to a shared config - @path = File.join(path, 'project.bundle') + @path_to_bundle = path_to_bundle end def restore - return false unless File.exists?(@path) - # Move repos dir to 'repositories.old' dir + return true unless File.exists?(@path) FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) - untar_xf(archive: @path, dir: path_to_repo) + + git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path: path_to_repo, bundle_path: @path_to_bundle) end private -- cgit v1.2.1 From 7204018a36e79a4b26b954965387495f50115541 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 12:54:12 +0200 Subject: fix path to bundle --- lib/gitlab/import_export/import_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 6878f4bbc8c..a4555699ff2 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -29,11 +29,11 @@ module Gitlab end def restore_repo - Gitlab::ImportExport::RepoRestorer.new(path: repo_path, project: project_tree.project).restore + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, project: project_tree.project).restore end def restore_wiki_repo - Gitlab::ImportExport::RepoRestorer.new(path: wiki_repo_path, project: project_tree.project).restore + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, project: project_tree.project).restore end def storage_path -- cgit v1.2.1 From 4b1fdfd6eb013f3549dc9198ffab46f7ef0de87c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 13:14:43 +0200 Subject: updated export file for testing --- .../import_export/test_project_export.tar.gz | Bin 352529 -> 338759 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 0c20898086d..87f118b6e23 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 28ba2176dc02563761df00dd48434158685d1daf Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 13:19:41 +0200 Subject: fix issue restoring repo --- lib/gitlab/import_export/repo_restorer.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index dc45238bf2a..9417393ecd1 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,18 +3,20 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, path_to_bundle: ) + def initialize(project:, path_to_bundle:) @project = project @path_to_bundle = path_to_bundle end def restore - return true unless File.exists?(@path) + return true unless File.exists?(@path_to_bundle) FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path: path_to_repo, bundle_path: @path_to_bundle) + rescue + false end private -- cgit v1.2.1 From f11920e21d35d8b34b9206a7ddf37422ca1914fa Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 14:21:27 +0200 Subject: more fixes - restoring repo --- lib/gitlab/import_export/import_service.rb | 4 ++-- lib/gitlab/import_export/repo_restorer.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index a4555699ff2..ae5a878d637 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -45,11 +45,11 @@ module Gitlab end def repo_path - File.join('storage_path', 'project.bundle') + File.join(storage_path, 'project.bundle') end def wiki_repo_path - File.join('storage_path', 'project.wiki.bundle') + File.join(storage_path, 'project.wiki.bundle') end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 9417393ecd1..7a4cda828b4 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -14,7 +14,7 @@ module Gitlab FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) - git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path: path_to_repo, bundle_path: @path_to_bundle) + git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) rescue false end -- cgit v1.2.1 From 9c60eca87fe30d34e29d26d667a9235912dcbe7e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 15:23:59 +0200 Subject: fix wiki import --- lib/gitlab/import_export/import_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index ae5a878d637..31b77981bad 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -33,7 +33,7 @@ module Gitlab end def restore_wiki_repo - Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, project: project_tree.project).restore + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, project: ProjectWiki.new(project_tree.project)).restore end def storage_path -- cgit v1.2.1 From 2a135866a24170e1db106885d4134e33141c07dd Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 16:24:09 +0200 Subject: added more tests to integration spec --- spec/features/projects/import_export/import_file_spec.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index b9173e44554..a7da7a21ea8 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -7,6 +7,7 @@ feature 'project import', feature: true, js: true do let!(:namespace) { create(:namespace, name: "asd", owner: user) } let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:project) { Project.last } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) login_as(user) @@ -32,8 +33,15 @@ feature 'project import', feature: true, js: true do click_on 'Continue to the next step' # import starts - expect(Project.last).not_to be_nil - expect(Project.last.issues).not_to be_empty - expect(Project.last.repo_exists?).to be true + expect(project).not_to be_nil + expect(project.issues).not_to be_empty + expect(project.merge_requests).not_to be_empty + expect(project.repo_exists?).to be true + expect(wiki_exists?).to be true + end + + def wiki_exists? + wiki = ProjectWiki.new(project) + File.exists?(wiki.repository.path_to_repo) && !wiki.repository.empty? end end -- cgit v1.2.1 From 9f5dd2de4e85eba70cd469b36db1c19988c3603f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 5 May 2016 18:12:24 +0200 Subject: handling errors a bit better on import failure --- app/workers/project_import_worker.rb | 18 +++++++++--------- lib/gitlab/import_export/import_service.rb | 14 ++++++++++---- lib/gitlab/import_export/project_tree_restorer.rb | 4 +++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb index aaa59f79d7f..cc75da704f6 100644 --- a/app/workers/project_import_worker.rb +++ b/app/workers/project_import_worker.rb @@ -11,15 +11,15 @@ class ProjectImportWorker owner: current_user, namespace_id: namespace_id, project_path: path) + if project + project.repository.after_import + project.import_finish + else + logger.error("There was an error during the import: #{tmpfile}") + end + end - # TODO: Move this to import service - # if result[:status] == :error - # project.update(import_error: result[:message]) - # project.import_fail - # return - # end - - project.repository.after_import - project.import_finish + def logger + Sidekiq.logger end end diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 31b77981bad..99fb9bad78d 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -14,7 +14,8 @@ module Gitlab end def execute - Gitlab::ImportExport::Importer.import(archive_file: @archive_file, storage_path: storage_path) + Gitlab::ImportExport::Importer.import(archive_file: @archive_file, + storage_path: storage_path) project_tree.project if [restore_project_tree, restore_repo, restore_wiki_repo].all? end @@ -25,15 +26,20 @@ module Gitlab end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, user: @current_user, project_path: @project_path, namespace_id: @namespace.id) + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, + user: @current_user, + project_path: @project_path, + namespace_id: @namespace.id) end def restore_repo - Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, project: project_tree.project).restore + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, + project: project_tree.project).restore end def restore_wiki_repo - Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, project: ProjectWiki.new(project_tree.project)).restore + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, + project: ProjectWiki.new(project_tree.project)).restore end def storage_path diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 0bc23dfaa24..7aa0ff46cde 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -14,6 +14,8 @@ module Gitlab @tree_hash = ActiveSupport::JSON.decode(json) @project_members = @tree_hash.delete('project_members') create_relations + rescue + false end def project @@ -50,7 +52,7 @@ module Gitlab project_params: project_params, user: @user, namespace_id: @namespace_id) project.path = @project_path project.name = @project_path - project.save + project.save! project end -- cgit v1.2.1 From ce598b05414d52b31437dbe19412eb6a9a16f1b5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 6 May 2016 13:40:02 +0200 Subject: fixed and refactored a few things based on MR feedback --- .../projects/import_export/export_service.rb | 5 +- lib/gitlab/import_export.rb | 2 +- lib/gitlab/import_export/command_line_util.rb | 27 ++++------ lib/gitlab/import_export/import_export.yml | 24 +++++---- lib/gitlab/import_export/import_export_reader.rb | 26 +++++----- lib/gitlab/import_export/project_tree_saver.rb | 12 ++--- .../import_export/import_export_reader_spec.rb | 4 +- .../import_export/project_tree_saver_spec.rb | 60 +++++++++++----------- spec/lib/gitlab/import_export/repo_bundler_spec.rb | 8 +-- .../gitlab/import_export/wiki_repo_bundler_spec.rb | 8 +-- spec/support/import_export/import_export.yml | 16 +++--- 11 files changed, 92 insertions(+), 100 deletions(-) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 47ceff18af6..fd569e1c9ae 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -5,10 +5,7 @@ module Projects def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) # TODO handle errors - save_project_tree - bundle_repo - bundle_wiki_repo - save_all + save_all if [save_project_tree, bundle_repo, bundle_wiki_repo].all? end private diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 539eae13f33..aa69f7c44a5 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -6,7 +6,7 @@ module Gitlab File.join(storage_path, relative_path) end - def project_atts + def project_attributes %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 5ff72f5ff8d..4a1018072c6 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -1,36 +1,31 @@ module Gitlab module ImportExport module CommandLineUtil - def tar_cf(archive:, dir:) - tar_with_options(archive: archive, dir: dir, options: 'cf') + def tar_czf(archive:, dir:) + tar_with_options(archive: archive, dir: dir, options: 'czf') end def untar_zxf(archive:, dir:) untar_with_options(archive: archive, dir: dir, options: 'zxf') end - def untar_xf(archive:, dir:) - untar_with_options(archive: archive, dir: dir, options: 'xf') - end - - def tar_czf(archive:, dir:) - tar_with_options(archive: archive, dir: dir, options: 'czf') + def git_bundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) end - def git_bundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) - cmd = %W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all) - _output, status = Gitlab::Popen.popen(cmd) - status.zero? + def git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) end def tar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} -C #{dir} .) - _output, status = Gitlab::Popen.popen(cmd) - status.zero? + execute(%W(tar -#{options} #{archive} -C #{dir} .)) end def untar_with_options(archive:, dir:, options:) - cmd = %W(tar -#{options} #{archive} -C #{dir}) + execute(%W(tar -#{options} #{archive} -C #{dir})) + end + + def execute(cmd) _output, status = Gitlab::Popen.popen(cmd) status.zero? end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 92f492e9013..ca433e72d5f 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -1,22 +1,23 @@ -# Class relationships to be included in the project import/export -:project_tree: - - :issues: +# Model relationships to be included in the project import/export +project_tree: + - issues: - :notes - :labels - :milestones - :snippets - :releases - :events - - :project_members: + - project_members: - :user - - :merge_requests: + - merge_requests: - :merge_request_diff - :notes - - :ci_commits: + - ci_commits: - :statuses -:attributes_only: - :project: +# Only include the following attributes for the models specified. +included_attributes: + project: - :name - :path - :description @@ -27,11 +28,12 @@ - :snippets_enabled - :visibility_level - :archived - :user: + user: - :id - :email - :username -:attributes_except: - :snippets: +# Do not include the following attributes for the models specified. +excluded_attributes: + snippets: - :expired_at \ No newline at end of file diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 14049cb1bd2..77db6cabe38 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -4,7 +4,7 @@ module Gitlab extend self def project_tree - { only: atts_only[:project], include: build_hash(tree) } + { only: included_attributes[:project], include: build_hash(tree) } end def tree @@ -14,24 +14,24 @@ module Gitlab private def config - @config ||= YAML.load_file('lib/gitlab/import_export/import_export.yml') + @config ||= YAML.load_file('lib/gitlab/import_export/import_export.yml').with_indifferent_access end - def atts_only - config[:attributes_only] + def included_attributes + config[:included_attributes] || {} end - def atts_except - config[:attributes_except] + def excluded_attributes + config[:excluded_attributes] || {} end def build_hash(array) - array.map do |el| - if el.is_a?(Hash) - process_include(el) + array.map do |model_object| + if model_object.is_a?(Hash) + process_include(model_object) else - only_except_hash = check_only_and_except(el) - only_except_hash.empty? ? el : { el => only_except_hash } + only_except_hash = check_only_and_except(model_object) + only_except_hash.empty? ? model_object : { model_object => only_except_hash } end end end @@ -82,12 +82,12 @@ module Gitlab def check_only(value) key = key_from_hash(value) - atts_only[key].nil? ? {} : { only: atts_only[key] } + included_attributes[key].nil? ? {} : { only: included_attributes[key] } end def check_except(value) key = key_from_hash(value) - atts_except[key].nil? ? {} : { except: atts_except[key] } + excluded_attributes[key].nil? ? {} : { except: excluded_attributes[key] } end def key_from_hash(value) diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 394411a56c2..29d715c16d3 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -3,19 +3,13 @@ module Gitlab class ProjectTreeSaver attr_reader :full_path - def initialize(project: , shared: ) + def initialize(project:, shared:) @project = project @export_path = shared.export_path - end - - def save @full_path = File.join(@export_path, project_filename) - save_to_disk end - private - - def save_to_disk + def save FileUtils.mkdir_p(@export_path) File.write(full_path, project_json_tree) true @@ -24,6 +18,8 @@ module Gitlab false end + private + # TODO remove magic keyword and move it to a shared config def project_filename "project.json" diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb index e71c7b60b80..0b2da73a225 100644 --- a/spec/lib/gitlab/import_export/import_export_reader_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -1,4 +1,4 @@ -require 'rspec' +require 'spec_helper' describe Gitlab::ImportExport::ImportExportReader do @@ -17,7 +17,7 @@ describe Gitlab::ImportExport::ImportExportReader do end it 'should generate hash from project tree config' do - allow(described_class).to receive(:config).and_return(YAML.load_file(test_config)) + allow(described_class).to receive(:config).and_return(YAML.load_file(test_config).deep_symbolize_keys) expect(described_class.project_tree).to eq(project_tree_hash) end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 5c1b9af2c13..67e6267b741 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -1,42 +1,20 @@ require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeSaver, services: true do - describe :save do + describe 'saves the project tree into a json object' do - # TODO refactor this into a setup method - - let(:user) { create(:user) } - let(:issue) { create(:issue, assignee: user) } - let(:merge_request) { create(:merge_request) } - let(:label) { create(:label) } - let(:snippet) { create(:project_snippet) } - let(:commit_status) { create(:commit_status) } - let(:release) { create(:release) } - let!(:project) do - create(:project, - :public, - name: 'searchable_project', - issues: [issue], - merge_requests: [merge_request], - labels: [label], - snippets: [snippet], - releases: [release] - ) - end - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) } - let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } - let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: shared) } - let!(:issue_note) { create(:note, note: ":+1: issue", noteable: issue) } - let!(:merge_request_note) { create(:note, note: ":+1: merge_request", noteable: merge_request) } + let(:project_tree_saver) { described_class.new(project: project, shared: shared) } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:user) { create(:user) } + let(:project) { setup_project } - before(:each) do + before do project.team << [user, :master] allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end - after(:each) do + after do FileUtils.rm_rf(export_path) end @@ -109,6 +87,30 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end end + def setup_project + issue = create(:issue, assignee: user) + merge_request = create(:merge_request) + label = create(:label) + snippet = create(:project_snippet) + commit_status = create(:commit_status) + release = create(:release) + + project = create(:project, + :public, + issues: [issue], + merge_requests: [merge_request], + labels: [label], + snippets: [snippet], + releases: [release] + ) + + create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) + create(:milestone, project: project) + create(:note, noteable: issue) + create(:note, noteable: merge_request) + project + end + def project_json(filename) JSON.parse(IO.read(filename)) end diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb index 23447d878a8..fa926bab823 100644 --- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb @@ -1,20 +1,20 @@ require 'spec_helper' describe Gitlab::ImportExport::RepoBundler, services: true do - describe :bundle do + describe 'bundle a project Git repo' do let(:user) { create(:user) } let!(:project) { create(:project, :public, name: 'searchable_project') } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:bundler) { Gitlab::ImportExport::RepoBundler.new(project: project, shared: shared) } + let(:bundler) { described_class.new(project: project, shared: shared) } - before(:each) do + before do project.team << [user, :master] allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) end - after(:each) do + after do FileUtils.rm_rf(export_path) end diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb index 6cc95edc217..60a0145c1a0 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' describe Gitlab::ImportExport::WikiRepoBundler, services: true do - describe :bundle do + describe 'bundle a wiki Git repo' do let(:user) { create(:user) } let!(:project) { create(:project, :public, name: 'searchable_project') } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:wiki_bundler) { Gitlab::ImportExport::WikiRepoBundler.new(project: project, shared: shared) } + let(:wiki_bundler) { described_class.new(project: project, shared: shared) } let!(:project_wiki) { ProjectWiki.new(project, user) } - before(:each) do + before do project.team << [user, :master] allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) project_wiki.wiki project_wiki.create_page("index", "test content") end - after(:each) do + after do FileUtils.rm_rf(export_path) end diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml index e6985d6413c..3ceec506401 100644 --- a/spec/support/import_export/import_export.yml +++ b/spec/support/import_export/import_export.yml @@ -1,20 +1,20 @@ # Class relationships to be included in the project import/export -:project_tree: +project_tree: - :issues - :labels - - :merge_requests: + - merge_requests: - :merge_request_diff - :merge_request_test - - :commit_statuses: + - commit_statuses: - :commit -:attributes_only: - :project: +included_attributes: + project: - :name - :path - :merge_requests: + merge_requests: - :id -:attributes_except: - :merge_requests: +excluded_attributes: + merge_requests: - :iid \ No newline at end of file -- cgit v1.2.1 From b6ab4a311396d12cdb686df885fe48c58ea31218 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 6 May 2016 14:39:57 +0200 Subject: missing private keyword --- lib/gitlab/import_export/command_line_util.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 4a1018072c6..c99b0d83b41 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -17,6 +17,8 @@ module Gitlab execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) end + private + def tar_with_options(archive:, dir:, options:) execute(%W(tar -#{options} #{archive} -C #{dir} .)) end -- cgit v1.2.1 From 49cdb778a17b25014c9439eb1a719e1fac9d04d0 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 6 May 2016 15:18:25 +0200 Subject: few fixes to import specs and code --- lib/gitlab/import_export/project.json | 434 --------------------- lib/gitlab/import_export/project_tree_restorer.rb | 5 +- .../gitlab/import_export/members_mapper_spec.rb | 4 +- spec/lib/gitlab/import_export/project.json | 434 +++++++++++++++++++++ .../import_export/project_tree_restorer_spec.rb | 4 +- 5 files changed, 441 insertions(+), 440 deletions(-) delete mode 100644 lib/gitlab/import_export/project.json create mode 100644 spec/lib/gitlab/import_export/project.json diff --git a/lib/gitlab/import_export/project.json b/lib/gitlab/import_export/project.json deleted file mode 100644 index 4a12c951dcb..00000000000 --- a/lib/gitlab/import_export/project.json +++ /dev/null @@ -1,434 +0,0 @@ -{ - "name": "searchable_project", - "path": "gitlabhq", - "description": null, - "issues_enabled": true, - "wall_enabled": false, - "merge_requests_enabled": true, - "wiki_enabled": true, - "snippets_enabled": true, - "visibility_level": 20, - "archived": false, - "issues": [ - { - "id": 1, - "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", - "assignee_id": 1, - "author_id": 2, - "project_id": 6, - "created_at": "2016-04-13T14:40:32.471Z", - "updated_at": "2016-04-13T14:40:38.956Z", - "position": 0, - "branch_name": null, - "description": null, - "milestone_id": null, - "state": "opened", - "iid": 1, - "updated_by_id": null, - "confidential": false, - "deleted_at": null, - "moved_to_id": null, - "notes": [ - { - "id": 1, - "note": ":+1: issue", - "noteable_type": "Issue", - "author_id": 21, - "created_at": "2016-04-13T14:40:38.944Z", - "updated_at": "2016-04-13T14:40:38.944Z", - "project_id": 8, - "attachment": { - "url": null - }, - "line_code": null, - "commit_id": null, - "noteable_id": 1, - "system": false, - "st_diff": null, - "updated_by_id": null, - "is_award": false - } - ] - } - ], - "labels": [ - { - "id": 1, - "title": "label1", - "color": "#990000", - "project_id": 6, - "created_at": "2016-04-13T14:40:34.704Z", - "updated_at": "2016-04-13T14:40:36.891Z", - "template": false, - "description": null - } - ], - "milestones": [ - { - "id": 1, - "title": "Milestone v1.2", - "project_id": 6, - "description": null, - "due_date": null, - "created_at": "2016-04-13T14:40:37.901Z", - "updated_at": "2016-04-13T14:40:37.901Z", - "state": "active", - "iid": 1 - } - ], - "snippets": [ - { - "id": 1, - "title": "Illo ipsa maxime magni aut.", - "content": "Excepturi delectus ut harum est molestiae dolor.", - "author_id": 10, - "project_id": 6, - "created_at": "2016-04-13T14:40:35.603Z", - "updated_at": "2016-04-13T14:40:36.903Z", - "file_name": "daphne.mraz", - "visibility_level": 0 - } - ], - "releases": [ - { - "id": 1, - "tag": "v1.1.0", - "description": "Awesome release", - "project_id": 6, - "created_at": "2016-04-13T14:40:36.223Z", - "updated_at": "2016-04-13T14:40:36.913Z" - } - ], - "events": [ - { - "id": 1, - "target_type": null, - "target_id": null, - "title": null, - "data": null, - "project_id": 6, - "created_at": "2016-04-13T14:40:40.122Z", - "updated_at": "2016-04-13T14:40:40.122Z", - "action": 8, - "author_id": 1 - } - ], - "project_members": [ - { - "id": 1, - "user": { - "id": 1, - "email": "norval.gulgowski@schambergerboyle.co.uk", - "created_at": "2016-04-13T14:40:30.963Z", - "updated_at": "2016-04-13T14:40:30.963Z", - "name": "Jalon Cormier DVM", - "admin": false, - "projects_limit": 42, - "skype": "", - "linkedin": "", - "twitter": "", - "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", - "theme_id": 2, - "bio": null, - "username": "vance.turner1", - "can_create_group": true, - "can_create_team": false, - "state": "active", - "color_scheme_id": 1, - "notification_level": 1, - "password_expires_at": null, - "created_by_id": null, - "last_credential_check_at": null, - "avatar": { - "url": null - }, - "hide_no_ssh_key": false, - "website_url": "", - "notification_email": "norval.gulgowski@schambergerboyle.co.uk", - "hide_no_password": false, - "password_automatically_set": false, - "location": null, - "encrypted_otp_secret": null, - "encrypted_otp_secret_iv": null, - "encrypted_otp_secret_salt": null, - "otp_required_for_login": false, - "otp_backup_codes": null, - "public_email": "", - "dashboard": "projects", - "project_view": "readme", - "consumed_timestep": null, - "layout": "fixed", - "hide_project_limit": false, - "otp_grace_period_started_at": null, - "ldap_email": false, - "external": false - } - } - ], - "merge_requests": [ - { - "id": 1, - "target_branch": "feature", - "source_branch": "master", - "source_project_id": 2, - "author_id": 5, - "assignee_id": null, - "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", - "created_at": "2016-04-13T14:40:33.381Z", - "updated_at": "2016-04-13T14:40:39.850Z", - "milestone_id": null, - "state": "opened", - "merge_status": "can_be_merged", - "target_project_id": 6, - "iid": 1, - "description": null, - "position": 0, - "locked_at": null, - "updated_by_id": null, - "merge_error": null, - "merge_params": { - }, - "merge_when_build_succeeds": false, - "merge_user_id": null, - "merge_commit_sha": null, - "deleted_at": null, - "merge_request_diff": { - "id": 1, - "state": "collected", - "st_commits": [ - { - "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" - ], - "authored_date": "2014-02-27T10:01:38.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T10:01:38.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - ], - "authored_date": "2014-02-27T09:57:31.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:57:31.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "d14d6c0abdd253381df51a723d58691b2ee1ab08" - ], - "authored_date": "2014-02-27T09:54:21.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:54:21.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" - ], - "authored_date": "2014-02-27T09:49:50.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:49:50.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" - ], - "authored_date": "2014-02-27T09:48:32.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:48:32.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - } - ], - "st_diffs": [ - { - "diff": "Binary files a/.DS_Store and /dev/null differ\n", - "new_path": ".DS_Store", - "old_path": ".DS_Store", - "a_mode": "100644", - "b_mode": "0", - "new_file": false, - "renamed_file": false, - "deleted_file": true, - "too_large": false - }, - { - "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", - "new_path": ".gitignore", - "old_path": ".gitignore", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", - "new_path": ".gitmodules", - "old_path": ".gitmodules", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", - "new_path": "files/.DS_Store", - "old_path": "files/.DS_Store", - "a_mode": "100644", - "b_mode": "0", - "new_file": false, - "renamed_file": false, - "deleted_file": true, - "too_large": false - }, - { - "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", - "new_path": "files/ruby/popen.rb", - "old_path": "files/ruby/popen.rb", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", - "new_path": "files/ruby/regex.rb", - "old_path": "files/ruby/regex.rb", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", - "new_path": "gitlab-grack", - "old_path": "gitlab-grack", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", - "new_path": "gitlab-shell", - "old_path": "gitlab-shell", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false, - "too_large": false - } - ], - "merge_request_id": 1, - "created_at": "2016-04-13T14:40:33.474Z", - "updated_at": "2016-04-13T14:40:33.834Z", - "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", - "real_size": "8" - }, - "notes": [ - { - "id": 2, - "note": ":+1: merge_request", - "noteable_type": "MergeRequest", - "author_id": 24, - "created_at": "2016-04-13T14:40:39.832Z", - "updated_at": "2016-04-13T14:40:39.832Z", - "project_id": 9, - "attachment": { - "url": null - }, - "line_code": null, - "commit_id": null, - "noteable_id": 1, - "system": false, - "st_diff": null, - "updated_by_id": null, - "is_award": false - } - ] - } - ], - "ci_commits": [ - { - "id": 2, - "project_id": 6, - "ref": "master", - "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "before_sha": null, - "push_data": null, - "created_at": "2016-04-13T14:40:37.759Z", - "updated_at": "2016-04-13T14:40:37.759Z", - "tag": false, - "yaml_errors": null, - "committed_at": null, - "gl_project_id": 6, - "statuses": [ - { - "id": 1, - "project_id": null, - "status": "success", - "finished_at": "2016-01-26T07:23:42.000Z", - "trace": null, - "created_at": "2016-04-13T14:40:37.717Z", - "updated_at": "2016-04-13T14:40:37.771Z", - "started_at": "2016-01-26T07:21:42.000Z", - "runner_id": null, - "coverage": null, - "commit_id": 2, - "commands": null, - "job_id": null, - "name": "default", - "deploy": false, - "options": null, - "allow_failure": false, - "stage": null, - "trigger_request_id": null, - "stage_idx": null, - "tag": null, - "ref": null, - "user_id": null, - "target_url": null, - "description": "commit status", - "artifacts_file": null, - "gl_project_id": 7, - "artifacts_metadata": null, - "erased_by_id": null, - "erased_at": null - } - ] - } - ] -} \ No newline at end of file diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 7aa0ff46cde..ad77f7f69b1 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -14,7 +14,8 @@ module Gitlab @tree_hash = ActiveSupport::JSON.decode(json) @project_members = @tree_hash.delete('project_members') create_relations - rescue + rescue => e + # TODO: handle errors better, move them to a shared thing false end @@ -83,7 +84,7 @@ module Gitlab def relation_from_factory(relation, relation_hash) Gitlab::ImportExport::RelationFactory.create( - relation_sym: relation, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) + relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) end end end diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 9175356c641..a4d3d6d122f 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::MembersMapper, services: true do - describe :map do + describe 'map members' do let(:user) { create(:user) } let(:project) { create(:project, :public, name: 'searchable_project') } @@ -31,7 +31,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end let(:members_mapper) do - Gitlab::ImportExport::MembersMapper.new( + described_class.new( exported_members: exported_members, user: user, project_id: project.id) end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json new file mode 100644 index 00000000000..4a12c951dcb --- /dev/null +++ b/spec/lib/gitlab/import_export/project.json @@ -0,0 +1,434 @@ +{ + "name": "searchable_project", + "path": "gitlabhq", + "description": null, + "issues_enabled": true, + "wall_enabled": false, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "visibility_level": 20, + "archived": false, + "issues": [ + { + "id": 1, + "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", + "assignee_id": 1, + "author_id": 2, + "project_id": 6, + "created_at": "2016-04-13T14:40:32.471Z", + "updated_at": "2016-04-13T14:40:38.956Z", + "position": 0, + "branch_name": null, + "description": null, + "milestone_id": null, + "state": "opened", + "iid": 1, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "notes": [ + { + "id": 1, + "note": ":+1: issue", + "noteable_type": "Issue", + "author_id": 21, + "created_at": "2016-04-13T14:40:38.944Z", + "updated_at": "2016-04-13T14:40:38.944Z", + "project_id": 8, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "labels": [ + { + "id": 1, + "title": "label1", + "color": "#990000", + "project_id": 6, + "created_at": "2016-04-13T14:40:34.704Z", + "updated_at": "2016-04-13T14:40:36.891Z", + "template": false, + "description": null + } + ], + "milestones": [ + { + "id": 1, + "title": "Milestone v1.2", + "project_id": 6, + "description": null, + "due_date": null, + "created_at": "2016-04-13T14:40:37.901Z", + "updated_at": "2016-04-13T14:40:37.901Z", + "state": "active", + "iid": 1 + } + ], + "snippets": [ + { + "id": 1, + "title": "Illo ipsa maxime magni aut.", + "content": "Excepturi delectus ut harum est molestiae dolor.", + "author_id": 10, + "project_id": 6, + "created_at": "2016-04-13T14:40:35.603Z", + "updated_at": "2016-04-13T14:40:36.903Z", + "file_name": "daphne.mraz", + "visibility_level": 0 + } + ], + "releases": [ + { + "id": 1, + "tag": "v1.1.0", + "description": "Awesome release", + "project_id": 6, + "created_at": "2016-04-13T14:40:36.223Z", + "updated_at": "2016-04-13T14:40:36.913Z" + } + ], + "events": [ + { + "id": 1, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 6, + "created_at": "2016-04-13T14:40:40.122Z", + "updated_at": "2016-04-13T14:40:40.122Z", + "action": 8, + "author_id": 1 + } + ], + "project_members": [ + { + "id": 1, + "user": { + "id": 1, + "email": "norval.gulgowski@schambergerboyle.co.uk", + "created_at": "2016-04-13T14:40:30.963Z", + "updated_at": "2016-04-13T14:40:30.963Z", + "name": "Jalon Cormier DVM", + "admin": false, + "projects_limit": 42, + "skype": "", + "linkedin": "", + "twitter": "", + "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", + "theme_id": 2, + "bio": null, + "username": "vance.turner1", + "can_create_group": true, + "can_create_team": false, + "state": "active", + "color_scheme_id": 1, + "notification_level": 1, + "password_expires_at": null, + "created_by_id": null, + "last_credential_check_at": null, + "avatar": { + "url": null + }, + "hide_no_ssh_key": false, + "website_url": "", + "notification_email": "norval.gulgowski@schambergerboyle.co.uk", + "hide_no_password": false, + "password_automatically_set": false, + "location": null, + "encrypted_otp_secret": null, + "encrypted_otp_secret_iv": null, + "encrypted_otp_secret_salt": null, + "otp_required_for_login": false, + "otp_backup_codes": null, + "public_email": "", + "dashboard": "projects", + "project_view": "readme", + "consumed_timestep": null, + "layout": "fixed", + "hide_project_limit": false, + "otp_grace_period_started_at": null, + "ldap_email": false, + "external": false + } + } + ], + "merge_requests": [ + { + "id": 1, + "target_branch": "feature", + "source_branch": "master", + "source_project_id": 2, + "author_id": 5, + "assignee_id": null, + "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", + "created_at": "2016-04-13T14:40:33.381Z", + "updated_at": "2016-04-13T14:40:39.850Z", + "milestone_id": null, + "state": "opened", + "merge_status": "can_be_merged", + "target_project_id": 6, + "iid": 1, + "description": null, + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "merge_request_diff": { + "id": 1, + "state": "collected", + "st_commits": [ + { + "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + ], + "authored_date": "2014-02-27T10:01:38.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T10:01:38.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + ], + "authored_date": "2014-02-27T09:57:31.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:57:31.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "d14d6c0abdd253381df51a723d58691b2ee1ab08" + ], + "authored_date": "2014-02-27T09:54:21.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:54:21.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" + ], + "authored_date": "2014-02-27T09:49:50.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:49:50.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:48:32.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:48:32.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "new_path": ".DS_Store", + "old_path": ".DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "new_path": ".gitignore", + "old_path": ".gitignore", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "new_path": ".gitmodules", + "old_path": ".gitmodules", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "new_path": "files/.DS_Store", + "old_path": "files/.DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", + "new_path": "files/ruby/popen.rb", + "old_path": "files/ruby/popen.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "new_path": "files/ruby/regex.rb", + "old_path": "files/ruby/regex.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "new_path": "gitlab-grack", + "old_path": "gitlab-grack", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "new_path": "gitlab-shell", + "old_path": "gitlab-shell", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 1, + "created_at": "2016-04-13T14:40:33.474Z", + "updated_at": "2016-04-13T14:40:33.834Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "8" + }, + "notes": [ + { + "id": 2, + "note": ":+1: merge_request", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-04-13T14:40:39.832Z", + "updated_at": "2016-04-13T14:40:39.832Z", + "project_id": 9, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 1, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false + } + ] + } + ], + "ci_commits": [ + { + "id": 2, + "project_id": 6, + "ref": "master", + "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "before_sha": null, + "push_data": null, + "created_at": "2016-04-13T14:40:37.759Z", + "updated_at": "2016-04-13T14:40:37.759Z", + "tag": false, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 6, + "statuses": [ + { + "id": 1, + "project_id": null, + "status": "success", + "finished_at": "2016-01-26T07:23:42.000Z", + "trace": null, + "created_at": "2016-04-13T14:40:37.717Z", + "updated_at": "2016-04-13T14:40:37.771Z", + "started_at": "2016-01-26T07:21:42.000Z", + "runner_id": null, + "coverage": null, + "commit_id": 2, + "commands": null, + "job_id": null, + "name": "default", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": null, + "trigger_request_id": null, + "stage_idx": null, + "tag": null, + "ref": null, + "user_id": null, + "target_url": null, + "description": "commit status", + "artifacts_file": null, + "gl_project_id": 7, + "artifacts_metadata": null, + "erased_by_id": null, + "erased_at": null + } + ] + } + ] +} \ No newline at end of file diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index aa8a12c4caa..0958365ff9e 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do - describe :restore do + describe 'restore project tree' do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } - let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(path: "lib/gitlab/import_export/", user: user, project_path: 'project', namespace_id: namespace.id) } + let(:project_tree_restorer) { described_class.new(path: Rails.root.join("spec/lib/gitlab/import_export/"), user: user, project_path: 'project', namespace_id: namespace.id) } context 'JSON' do let(:restored_project_json) do -- cgit v1.2.1 From 8ac53eb5d0dcc6d0248a8b178a3c1b5f2d2284e1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 6 May 2016 17:55:06 +0200 Subject: started refactoring import export reader - WIP --- lib/gitlab/import_export.rb | 2 +- lib/gitlab/import_export/attributes_finder.rb | 35 +++++++++++ lib/gitlab/import_export/import_export_reader.rb | 68 +++++++--------------- .../import_export/import_export_reader_spec.rb | 4 +- 4 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 lib/gitlab/import_export/attributes_finder.rb diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index aa69f7c44a5..84860b43cbe 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -11,7 +11,7 @@ module Gitlab end def project_tree - Gitlab::ImportExport::ImportExportReader.project_tree + Gitlab::ImportExport::ImportExportReader.new.project_tree end def storage_path diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb new file mode 100644 index 00000000000..12ae79a8773 --- /dev/null +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -0,0 +1,35 @@ +module Gitlab + module ImportExport + class AttributesFinder + def initialize(included_attributes:, excluded_attributes:) + @included_attributes = included_attributes || {} + @excluded_attributes = excluded_attributes || {} + end + + def find(model_object) + parsed_hash = find_attributes_only(model_object) + parsed_hash.empty? ? model_object : { model_object => parsed_hash } + end + + def find_attributes_only(value) + find_included(value).merge(find_excluded(value)) + end + + def find_included(value) + key = key_from_hash(value) + @included_attributes[key].nil? ? {} : { only: @included_attributes[key] } + end + + def find_excluded(value) + key = key_from_hash(value) + @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] } + end + + private + + def key_from_hash(value) + value.is_a?(Hash) ? value.keys.first : value + end + end + end +end \ No newline at end of file diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 77db6cabe38..a6cf1910105 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -1,37 +1,27 @@ module Gitlab module ImportExport - module ImportExportReader - extend self + class ImportExportReader + #FIXME - def project_tree - { only: included_attributes[:project], include: build_hash(tree) } + def initialize(config: 'lib/gitlab/import_export/import_export.yml') + config = YAML.load_file('lib/gitlab/import_export/import_export.yml').with_indifferent_access + @tree = config[:project_tree] + @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config[:included_attributes], + excluded_attributes: config[:excluded_attributes]) end - def tree - config[:project_tree] + def project_tree + { only: @attributes_parser.find_included(:project), include: build_hash(@tree) } end private - def config - @config ||= YAML.load_file('lib/gitlab/import_export/import_export.yml').with_indifferent_access - end - - def included_attributes - config[:included_attributes] || {} - end - - def excluded_attributes - config[:excluded_attributes] || {} - end - - def build_hash(array) - array.map do |model_object| + def build_hash(model_list) + model_list.map do |model_object| if model_object.is_a?(Hash) process_include(model_object) else - only_except_hash = check_only_and_except(model_object) - only_except_hash.empty? ? model_object : { model_object => only_except_hash } + @attributes_parser.find(model_object) end end end @@ -51,48 +41,30 @@ module Gitlab def process_current_class(hash, included_classes_hash, value) value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value - only_except_hash = check_only_and_except(hash.keys.first) - included_classes_hash[hash.keys.first] ||= only_except_hash unless only_except_hash.empty? + attributes_hash = @attributes_parser.find_attributes_only(hash.keys.first) + included_classes_hash[hash.keys.first] ||= attributes_hash unless attributes_hash.empty? value end def add_new_class(current_key, included_classes_hash, value) - only_except_hash = check_only_and_except(value) + attributes_hash = @attributes_parser.find_attributes_only(value) parsed_hash = { include: value } - unless only_except_hash.empty? + unless attributes_hash.empty? if value.is_a?(Hash) - parsed_hash = { include: value.merge(only_except_hash) } + parsed_hash = { include: value.merge(attributes_hash) } else - parsed_hash = { include: { value => only_except_hash } } + parsed_hash = { include: { value => attributes_hash } } end end included_classes_hash[current_key] = parsed_hash end def add_to_class(current_key, included_classes_hash, value) - only_except_hash = check_only_and_except(value) - value = { value => only_except_hash } unless only_except_hash.empty? + attributes_hash = @attributes_parser.find_attributes_only(value) + value = { value => attributes_hash } unless attributes_hash.empty? old_values = included_classes_hash[current_key][:include] included_classes_hash[current_key][:include] = ([old_values] + [value]).compact.flatten end - - def check_only_and_except(value) - check_only(value).merge(check_except(value)) - end - - def check_only(value) - key = key_from_hash(value) - included_attributes[key].nil? ? {} : { only: included_attributes[key] } - end - - def check_except(value) - key = key_from_hash(value) - excluded_attributes[key].nil? ? {} : { except: excluded_attributes[key] } - end - - def key_from_hash(value) - value.is_a?(Hash) ? value.keys.first : value - end end end end diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb index 0b2da73a225..6be2289b5a0 100644 --- a/spec/lib/gitlab/import_export/import_export_reader_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -17,8 +17,6 @@ describe Gitlab::ImportExport::ImportExportReader do end it 'should generate hash from project tree config' do - allow(described_class).to receive(:config).and_return(YAML.load_file(test_config).deep_symbolize_keys) - - expect(described_class.project_tree).to eq(project_tree_hash) + expect(described_class.new(config: test_config).project_tree).to eq(project_tree_hash) end end -- cgit v1.2.1 From 3aee167dcc26e6b9b92d6f67b316280675f5cde8 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 11:10:12 +0200 Subject: fixed issues after refactor, spec passing --- lib/gitlab/import_export/import_export_reader.rb | 10 +++++----- spec/lib/gitlab/import_export/import_export_reader_spec.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index a6cf1910105..3f45de209cc 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -4,14 +4,14 @@ module Gitlab #FIXME def initialize(config: 'lib/gitlab/import_export/import_export.yml') - config = YAML.load_file('lib/gitlab/import_export/import_export.yml').with_indifferent_access - @tree = config[:project_tree] - @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config[:included_attributes], - excluded_attributes: config[:excluded_attributes]) + config_hash = YAML.load_file(config).with_indifferent_access + @tree = config_hash[:project_tree] + @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], + excluded_attributes: config_hash[:excluded_attributes]) end def project_tree - { only: @attributes_parser.find_included(:project), include: build_hash(@tree) } + @attributes_parser.find_included(:project).merge(include: build_hash(@tree)) end private diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb index 6be2289b5a0..f826c5ec8e6 100644 --- a/spec/lib/gitlab/import_export/import_export_reader_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -17,6 +17,6 @@ describe Gitlab::ImportExport::ImportExportReader do end it 'should generate hash from project tree config' do - expect(described_class.new(config: test_config).project_tree).to eq(project_tree_hash) + expect(described_class.new(config: test_config).project_tree) =~ (project_tree_hash) end end -- cgit v1.2.1 From 9c639041fe312f8edf1613809ac665cb8755ffb7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 12:15:50 +0200 Subject: bit more refactoring of import export reader, fixed rubocop warning --- lib/gitlab/import_export/attributes_finder.rb | 2 +- lib/gitlab/import_export/import_export_reader.rb | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index 12ae79a8773..8a599ad5261 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -32,4 +32,4 @@ module Gitlab end end end -end \ No newline at end of file +end diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 3f45de209cc..35ff05350d8 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -17,30 +17,30 @@ module Gitlab private def build_hash(model_list) - model_list.map do |model_object| - if model_object.is_a?(Hash) - process_include(model_object) + model_list.map do |model_object_hash| + if model_object_hash.is_a?(Hash) + process_model_object(model_object_hash) else - @attributes_parser.find(model_object) + @attributes_parser.find(model_object_hash) end end end - def process_include(hash, included_classes_hash = {}) - hash.values.flatten.each do |value| - current_key = hash.keys.first - value = process_current_class(hash, included_classes_hash, value) + def process_model_object(model_object_hash, included_classes_hash = {}) + model_object_hash.values.flatten.each do |model_object| + current_key = model_object_hash.keys.first + model_object = process_current_class(model_object_hash, included_classes_hash, model_object) if included_classes_hash[current_key] - add_to_class(current_key, included_classes_hash, value) + add_to_class(current_key, included_classes_hash, model_object) else - add_new_class(current_key, included_classes_hash, value) + add_new_class(current_key, included_classes_hash, model_object) end end included_classes_hash end def process_current_class(hash, included_classes_hash, value) - value = value.is_a?(Hash) ? process_include(hash, included_classes_hash) : value + value = value.is_a?(Hash) ? process_model_object(hash, included_classes_hash) : value attributes_hash = @attributes_parser.find_attributes_only(hash.keys.first) included_classes_hash[hash.keys.first] ||= attributes_hash unless attributes_hash.empty? value -- cgit v1.2.1 From 7ff2a51eb24032895e6199c62de44e17de65af7b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 17:58:43 +0200 Subject: more refactoring to import export reader --- lib/gitlab/import_export/attributes_finder.rb | 9 +++- lib/gitlab/import_export/import_export_reader.rb | 56 ++++++++++++------------ 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index 8a599ad5261..cfb9f0eef18 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -11,8 +11,9 @@ module Gitlab parsed_hash.empty? ? model_object : { model_object => parsed_hash } end - def find_attributes_only(value) - find_included(value).merge(find_excluded(value)) + def parse(model_object) + parsed_hash = find_attributes_only(model_object) + yield parsed_hash unless parsed_hash.empty? end def find_included(value) @@ -27,6 +28,10 @@ module Gitlab private + def find_attributes_only(value) + find_included(value).merge(find_excluded(value)) + end + def key_from_hash(value) value.is_a?(Hash) ? value.keys.first : value end diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 35ff05350d8..84c3f3e9729 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -1,13 +1,13 @@ module Gitlab module ImportExport class ImportExportReader - #FIXME def initialize(config: 'lib/gitlab/import_export/import_export.yml') config_hash = YAML.load_file(config).with_indifferent_access @tree = config_hash[:project_tree] @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], excluded_attributes: config_hash[:excluded_attributes]) + @json_config_hash = {} end def project_tree @@ -19,51 +19,49 @@ module Gitlab def build_hash(model_list) model_list.map do |model_object_hash| if model_object_hash.is_a?(Hash) - process_model_object(model_object_hash) + build_json_config_hash(model_object_hash) else @attributes_parser.find(model_object_hash) end end end - def process_model_object(model_object_hash, included_classes_hash = {}) + def build_json_config_hash(model_object_hash) model_object_hash.values.flatten.each do |model_object| current_key = model_object_hash.keys.first - model_object = process_current_class(model_object_hash, included_classes_hash, model_object) - if included_classes_hash[current_key] - add_to_class(current_key, included_classes_hash, model_object) - else - add_new_class(current_key, included_classes_hash, model_object) - end + + @attributes_parser.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } + + handle_model_object(current_key, model_object) end - included_classes_hash + @json_config_hash end - def process_current_class(hash, included_classes_hash, value) - value = value.is_a?(Hash) ? process_model_object(hash, included_classes_hash) : value - attributes_hash = @attributes_parser.find_attributes_only(hash.keys.first) - included_classes_hash[hash.keys.first] ||= attributes_hash unless attributes_hash.empty? - value + def handle_model_object(current_key, model_object) + if @json_config_hash[current_key] + add_model_value(current_key, model_object) + else + create_model_value(current_key, model_object) + end end - def add_new_class(current_key, included_classes_hash, value) - attributes_hash = @attributes_parser.find_attributes_only(value) + def create_model_value(current_key, value) parsed_hash = { include: value } - unless attributes_hash.empty? - if value.is_a?(Hash) - parsed_hash = { include: value.merge(attributes_hash) } - else - parsed_hash = { include: { value => attributes_hash } } - end + + @attributes_parser.parse(value) do |hash| + parsed_hash = { include: hash_or_merge(value, hash) } end - included_classes_hash[current_key] = parsed_hash + @json_config_hash[current_key] = parsed_hash + end + + def add_model_value(current_key, value) + @attributes_parser.parse(value) { |hash| value = { value => hash } } + old_values = @json_config_hash[current_key][:include] + @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten end - def add_to_class(current_key, included_classes_hash, value) - attributes_hash = @attributes_parser.find_attributes_only(value) - value = { value => attributes_hash } unless attributes_hash.empty? - old_values = included_classes_hash[current_key][:include] - included_classes_hash[current_key][:include] = ([old_values] + [value]).compact.flatten + def hash_or_merge(value, hash) + value.is_a?(Hash) ? value.merge(hash) : hash end end end -- cgit v1.2.1 From bd0c8ce40542dd3c05526c4c80df4ad4c151b4b6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 18:07:29 +0200 Subject: fixed space --- spec/features/projects/import_export/import_file_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index a7da7a21ea8..03298149b60 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -8,6 +8,7 @@ feature 'project import', feature: true, js: true do let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } let(:project) { Project.last } + background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) login_as(user) -- cgit v1.2.1 From 5dad9f1fcbd432f1c2cc2e861983e4c49cfce140 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 16:19:07 +0000 Subject: new line missing --- lib/gitlab/import_export/attributes_finder.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index cfb9f0eef18..3439ef1006e 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -1,6 +1,7 @@ module Gitlab module ImportExport class AttributesFinder + def initialize(included_attributes:, excluded_attributes:) @included_attributes = included_attributes || {} @excluded_attributes = excluded_attributes || {} -- cgit v1.2.1 From 21be0cae62af3ba5e0e71758d260287b164fe504 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 18:33:48 +0200 Subject: fixing merge issues --- lib/gitlab/import_export.rb | 4 ---- lib/gitlab/import_export/import_export.yml | 1 - 2 files changed, 5 deletions(-) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 84860b43cbe..655373c03af 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -6,10 +6,6 @@ module Gitlab File.join(storage_path, relative_path) end - def project_attributes - %i(name path description issues_enabled wall_enabled merge_requests_enabled wiki_enabled snippets_enabled visibility_level archived) - end - def project_tree Gitlab::ImportExport::ImportExportReader.new.project_tree end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ca433e72d5f..36128909df4 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -22,7 +22,6 @@ included_attributes: - :path - :description - :issues_enabled - - :wall_enabled - :merge_requests_enabled - :wiki_enabled - :snippets_enabled -- cgit v1.2.1 From 6a12ff6345e517af9cf07cb61f3a0ea85562f399 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 May 2016 18:40:31 +0200 Subject: renaming variable --- lib/gitlab/import_export/import_export_reader.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 84c3f3e9729..37093ff58ed 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -17,11 +17,11 @@ module Gitlab private def build_hash(model_list) - model_list.map do |model_object_hash| - if model_object_hash.is_a?(Hash) - build_json_config_hash(model_object_hash) + model_list.map do |model_objects| + if model_objects.is_a?(Hash) + build_json_config_hash(model_objects) else - @attributes_parser.find(model_object_hash) + @attributes_parser.find(model_objects) end end end -- cgit v1.2.1 From 5b51d13c5e432cb76dc911047cc8c5932ecfe421 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 10 May 2016 12:01:10 +0200 Subject: removed method no longer needed --- lib/gitlab/import_export.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 22973ff3b6a..655373c03af 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -6,10 +6,6 @@ module Gitlab File.join(storage_path, relative_path) end - def project_tree_list - project_tree.map {|r| r.is_a?(Hash) ? r.keys.first : r } - end - def project_tree Gitlab::ImportExport::ImportExportReader.new.project_tree end -- cgit v1.2.1 From e6fcd6d5bd8580f992f557c05aef3cc335b7ee8b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 10 May 2016 12:30:45 +0200 Subject: fixed more import merging issues --- spec/lib/gitlab/import_export/project.json | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 4a12c951dcb..8b6a16c049f 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -3,7 +3,6 @@ "path": "gitlabhq", "description": null, "issues_enabled": true, - "wall_enabled": false, "merge_requests_enabled": true, "wiki_enabled": true, "snippets_enabled": true, -- cgit v1.2.1 From 17ce63a5e19f03fb1fec04724ca6d41e463a32b6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 10 May 2016 13:11:21 +0200 Subject: updated spec export file --- .../import_export/test_project_export.tar.gz | Bin 338759 -> 322702 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 87f118b6e23..8147c289b8e 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From a5d59f075a4a9a301ef985eb7cc6cdfdf3e73955 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 10 May 2016 17:15:20 +0200 Subject: added better error handling. Also refactored some of the code and fixed a few issues in project_tree_saver --- app/services/projects/import_export/export_service.rb | 8 ++++++-- lib/gitlab/import_export.rb | 4 ---- lib/gitlab/import_export/error.rb | 5 +++++ lib/gitlab/import_export/import_export_reader.rb | 12 ++++++++---- lib/gitlab/import_export/project_tree_saver.rb | 12 ++++++------ lib/gitlab/import_export/repo_bundler.rb | 9 +++++---- lib/gitlab/import_export/saver.rb | 17 ++++++++++------- lib/gitlab/import_export/shared.rb | 15 +++++++++++++++ lib/gitlab/import_export/wiki_repo_bundler.rb | 7 ++++--- .../gitlab/import_export/import_export_reader_spec.rb | 4 ++-- 10 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 lib/gitlab/import_export/error.rb diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index fd569e1c9ae..87a259ed15a 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -4,8 +4,8 @@ module Projects def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) - # TODO handle errors save_all if [save_project_tree, bundle_repo, bundle_wiki_repo].all? + notify_worker if @shared.errors.any? end private @@ -23,7 +23,11 @@ module Projects end def save_all - Gitlab::ImportExport::Saver.save(storage_path: @shared.export_path) + Gitlab::ImportExport::Saver.save(shared: @shared) + end + + def notify_worker + raise Gitlab::ImportExport::Error @shared.errors.join(', ') end end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 655373c03af..6835411ba70 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -6,10 +6,6 @@ module Gitlab File.join(storage_path, relative_path) end - def project_tree - Gitlab::ImportExport::ImportExportReader.new.project_tree - end - def storage_path File.join(Settings.shared['path'], 'tmp/project_exports') end diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb new file mode 100644 index 00000000000..b47a646c0b9 --- /dev/null +++ b/lib/gitlab/import_export/error.rb @@ -0,0 +1,5 @@ +module Gitlab + module ImportExport + class Error < StandardError; end + end +end \ No newline at end of file diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 37093ff58ed..b7204d5a87c 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -2,16 +2,18 @@ module Gitlab module ImportExport class ImportExportReader - def initialize(config: 'lib/gitlab/import_export/import_export.yml') - config_hash = YAML.load_file(config).with_indifferent_access + def initialize(config: 'lib/gitlab/import_export/import_export.yml', shared:) + @shared = shared + config_hash = YAML.load_file(config).deep_symbolize_keys @tree = config_hash[:project_tree] @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], excluded_attributes: config_hash[:excluded_attributes]) - @json_config_hash = {} end def project_tree @attributes_parser.find_included(:project).merge(include: build_hash(@tree)) + rescue => e + @shared.error(e.message) end private @@ -27,6 +29,8 @@ module Gitlab end def build_json_config_hash(model_object_hash) + @json_config_hash = {} + model_object_hash.values.flatten.each do |model_object| current_key = model_object_hash.keys.first @@ -61,7 +65,7 @@ module Gitlab end def hash_or_merge(value, hash) - value.is_a?(Hash) ? value.merge(hash) : hash + value.is_a?(Hash) ? value.merge(hash) : { value => hash } end end end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 29d715c16d3..2287524c8a5 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -5,16 +5,16 @@ module Gitlab def initialize(project:, shared:) @project = project - @export_path = shared.export_path - @full_path = File.join(@export_path, project_filename) + @shared = shared + @full_path = File.join(@shared.export_path, project_filename) end def save - FileUtils.mkdir_p(@export_path) + FileUtils.mkdir_p(@shared.export_path) File.write(full_path, project_json_tree) true - rescue - # TODO: handle error + rescue => e + @shared.error(e.message) false end @@ -26,7 +26,7 @@ module Gitlab end def project_json_tree - @project.to_json(Gitlab::ImportExport.project_tree) + @project.to_json(Gitlab::ImportExport::ImportExportReader.new(shared: @shared).project_tree) end end end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index cd871687d76..bcf976fb624 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -7,21 +7,22 @@ module Gitlab def initialize(project: , shared: ) @project = project - @export_path = shared.export_path + @shared = shared end def bundle return false if @project.empty_repo? - @full_path = File.join(@export_path, project_filename) + @full_path = File.join(@shared.export_path, project_filename) bundle_to_disk end private def bundle_to_disk - FileUtils.mkdir_p(@export_path) + FileUtils.mkdir_p(@shared.export_path) git_bundle(repo_path: path_to_repo, bundle_path: @full_path) - rescue + rescue => e + @shared.error(e.message) false end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index 634e58e6039..024a0e1e785 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -7,32 +7,35 @@ module Gitlab new(*args).save end - def initialize(storage_path:) - @storage_path = storage_path + def initialize(shared:) + @shared = shared end def save if compress_and_save - remove_storage_path + remove_@shared.storage_path Rails.logger.info("Saved project export #{archive_file}") archive_file else false end + rescue => e + @shared.error(e.message) + false end private def compress_and_save - tar_czf(archive: archive_file, dir: @storage_path) + tar_czf(archive: archive_file, dir: @shared.storage_path) end - def remove_storage_path - FileUtils.rm_rf(@storage_path) + def remove_shared.storage_path + FileUtils.rm_rf(@shared.storage_path) end def archive_file - @archive_file ||= File.join(@storage_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") + @archive_file ||= File.join(@shared.storage_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") end end end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index a4ec33a6cf7..4246f73af90 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -1,13 +1,28 @@ module Gitlab module ImportExport class Shared + + attr_reader :errors + def initialize(opts) @opts = opts + @errors = [] end def export_path @export_path ||= Gitlab::ImportExport.export_path(relative_path: @opts[:relative_path]) end + + def error(message) + error_out(message, caller[0].dup) + @errors << message + end + + private + + def error_out(message, caller) + Rails.logger.error("Import/Export error raised on #{caller}: #{message}") + end end end end diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb index 17f1530d5a0..a0000176bb5 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -4,14 +4,15 @@ module Gitlab def bundle @wiki = ProjectWiki.new(@project) return true if !wiki? # it's okay to have no Wiki - @full_path = File.join(@export_path, project_filename) + @full_path = File.join(@shared.export_path, project_filename) bundle_to_disk end def bundle_to_disk - FileUtils.mkdir_p(@export_path) + FileUtils.mkdir_p(@shared.export_path) git_bundle(repo_path: path_to_repo, bundle_path: @full_path) - rescue + rescue => e + @shared.error(e.message) false end diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb index f826c5ec8e6..2fc9a39c68b 100644 --- a/spec/lib/gitlab/import_export/import_export_reader_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::ImportExportReader do - + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path:'') } let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do { @@ -17,6 +17,6 @@ describe Gitlab::ImportExport::ImportExportReader do end it 'should generate hash from project tree config' do - expect(described_class.new(config: test_config).project_tree) =~ (project_tree_hash) + expect(described_class.new(config: test_config, shared: shared).project_tree) =~ (project_tree_hash) end end -- cgit v1.2.1 From 49e6fc40b9d23b4081139a3ad141722a6b1f7cfc Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 13:17:49 +0200 Subject: fix bad refactor --- lib/gitlab/import_export/saver.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index 024a0e1e785..7532c977fcc 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -13,7 +13,7 @@ module Gitlab def save if compress_and_save - remove_@shared.storage_path + remove_storage_path Rails.logger.info("Saved project export #{archive_file}") archive_file else @@ -30,7 +30,7 @@ module Gitlab tar_czf(archive: archive_file, dir: @shared.storage_path) end - def remove_shared.storage_path + def remove_storage_path FileUtils.rm_rf(@shared.storage_path) end -- cgit v1.2.1 From 5086e899fe26447e6e24064c9a05d3989c1e8448 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 13:21:00 +0200 Subject: fix log statement --- app/workers/project_import_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb index cc75da704f6..c6a6d24b4e5 100644 --- a/app/workers/project_import_worker.rb +++ b/app/workers/project_import_worker.rb @@ -15,7 +15,7 @@ class ProjectImportWorker project.repository.after_import project.import_finish else - logger.error("There was an error during the import: #{tmpfile}") + logger.error("There was an error during the import: #{tmp_file}") end end -- cgit v1.2.1 From cffae0d22ee159b78f66eb68cae9df4bcb9e83e5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 14:51:25 +0200 Subject: fixing more export problems --- app/services/projects/import_export/export_service.rb | 2 +- app/workers/project_export_worker.rb | 3 +-- lib/gitlab/import_export/saver.rb | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 87a259ed15a..e6eb6f915b0 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -27,7 +27,7 @@ module Projects end def notify_worker - raise Gitlab::ImportExport::Error @shared.errors.join(', ') + raise Gitlab::ImportExport::Error.new(@shared.errors.join(', ')) end end end diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index a1add5395b7..3616b37d2ad 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -1,13 +1,12 @@ class ProjectExportWorker include Sidekiq::Worker - # TODO: enabled retry - disabled for QA purposes + # TODO: enable retry - disabled for QA purposes sidekiq_options queue: :gitlab_shell, retry: false def perform(current_user_id, project_id) current_user = User.find(current_user_id) project = Project.find(project_id) ::Projects::ImportExport::ExportService.new(project, current_user).execute - # TODO : Handle errors end end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index 7532c977fcc..a2ff43a3ce3 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -13,7 +13,7 @@ module Gitlab def save if compress_and_save - remove_storage_path + remove_export_path Rails.logger.info("Saved project export #{archive_file}") archive_file else @@ -27,15 +27,15 @@ module Gitlab private def compress_and_save - tar_czf(archive: archive_file, dir: @shared.storage_path) + tar_czf(archive: archive_file, dir: @shared.export_path) end - def remove_storage_path - FileUtils.rm_rf(@shared.storage_path) + def remove_export_path + FileUtils.rm_rf(@shared.export_path) end def archive_file - @archive_file ||= File.join(@shared.storage_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") + @archive_file ||= File.join(@shared.export_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz") end end end -- cgit v1.2.1 From ae53840723208d34b78dd67f7aedf9c627af4ed1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 15:02:55 +0200 Subject: updated export file for spec --- .../import_export/test_project_export.tar.gz | Bin 322702 -> 338822 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 8147c289b8e..f2c3c3cd2dc 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From a61456e44e8f26067187643a3f1b74b484428bad Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 11 May 2016 17:22:45 +0200 Subject: refactored import to use shared error stuff and fixed a few issues with recent refactorings --- app/workers/project_import_worker.rb | 1 - lib/gitlab/import_export/import_export_reader.rb | 2 ++ lib/gitlab/import_export/import_service.rb | 19 ++++++++----------- lib/gitlab/import_export/importer.rb | 11 +++++++---- lib/gitlab/import_export/project_tree_restorer.rb | 13 ++++++++----- lib/gitlab/import_export/repo_restorer.rb | 6 ++++-- lib/gitlab/import_export/shared.rb | 2 +- .../import_export/project_tree_restorer_spec.rb | 3 ++- 8 files changed, 32 insertions(+), 25 deletions(-) diff --git a/app/workers/project_import_worker.rb b/app/workers/project_import_worker.rb index c6a6d24b4e5..b2902c3278e 100644 --- a/app/workers/project_import_worker.rb +++ b/app/workers/project_import_worker.rb @@ -13,7 +13,6 @@ class ProjectImportWorker project_path: path) if project project.repository.after_import - project.import_finish else logger.error("There was an error during the import: #{tmp_file}") end diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index b7204d5a87c..f637756b35f 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -2,6 +2,8 @@ module Gitlab module ImportExport class ImportExportReader + attr_reader :tree + def initialize(config: 'lib/gitlab/import_export/import_export.yml', shared:) @shared = shared config_hash = YAML.load_file(config).deep_symbolize_keys diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 99fb9bad78d..f7b66448abc 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -10,12 +10,12 @@ module Gitlab @archive_file = archive_file @current_user = owner @namespace = Namespace.find(namespace_id) - @project_path = project_path + @shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace, project_path: project_path) end def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, - storage_path: storage_path) + shared: @shared) project_tree.project if [restore_project_tree, restore_repo, restore_wiki_repo].all? end @@ -26,36 +26,33 @@ module Gitlab end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(path: storage_path, - user: @current_user, - project_path: @project_path, + @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: @current_user, + shared: @shared, namespace_id: @namespace.id) end def restore_repo Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, + shared: @shared, project: project_tree.project).restore end def restore_wiki_repo Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, + shared: @shared, project: ProjectWiki.new(project_tree.project)).restore end - def storage_path - @storage_path ||= Gitlab::ImportExport.export_path(relative_path: path_with_namespace) - end - def path_with_namespace File.join(@namespace.path, @project_path) end def repo_path - File.join(storage_path, 'project.bundle') + File.join(@shared.export_path, 'project.bundle') end def wiki_repo_path - File.join(storage_path, 'project.wiki.bundle') + File.join(@shared.export_path, 'project.wiki.bundle') end end end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 8f838287f97..19c5aafad20 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -7,20 +7,23 @@ module Gitlab new(*args).import end - def initialize(archive_file: , storage_path:) + def initialize(archive_file: , shared:) @archive_file = archive_file - @storage_path = storage_path + @shared = shared end def import - FileUtils.mkdir_p(@storage_path) + FileUtils.mkdir_p(@shared.storage_path) decompress_archive + rescue => e + @shared.error(e.message) + false end private def decompress_archive - untar_zxf(archive: @archive_file, dir: @storage_path) + untar_zxf(archive: @archive_file, dir: @shared.storage_path) end end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index ad77f7f69b1..06bf86a1787 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -2,11 +2,12 @@ module Gitlab module ImportExport class ProjectTreeRestorer - def initialize(path:, user:, project_path:, namespace_id:) - @path = File.join(path, 'project.json') + def initialize(user:, shared:, namespace_id:) + @path = File.join(shared.export_path, 'project.json') @user = user - @project_path = project_path + @project_path = shared.opts[:project_path] @namespace_id = namespace_id + @shared = shared end def restore @@ -15,7 +16,7 @@ module Gitlab @project_members = @tree_hash.delete('project_members') create_relations rescue => e - # TODO: handle errors better, move them to a shared thing + @shared.error(e.message) false end @@ -44,7 +45,9 @@ module Gitlab end def default_relation_list - Gitlab::ImportExport::ImportExportReader.tree.reject { |model| model.is_a?(Hash) && model[:project_members] } + Gitlab::ImportExport::ImportExportReader.new(shared: @shared).tree.reject do |model| + model.is_a?(Hash) && model[:project_members] + end end def create_project diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 7a4cda828b4..3909d447448 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,9 +3,10 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, path_to_bundle:) + def initialize(project:, shared:, path_to_bundle:) @project = project @path_to_bundle = path_to_bundle + @shared = shared end def restore @@ -15,7 +16,8 @@ module Gitlab FileUtils.mkdir_p(path_to_repo) git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) - rescue + rescue => e + @shared.error(e.message) false end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 4246f73af90..050b0428f45 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -2,7 +2,7 @@ module Gitlab module ImportExport class Shared - attr_reader :errors + attr_reader :errors, :opts def initialize(opts) @opts = opts diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 0958365ff9e..7c6a14fc8a8 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -5,7 +5,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } - let(:project_tree_restorer) { described_class.new(path: Rails.root.join("spec/lib/gitlab/import_export/"), user: user, project_path: 'project', namespace_id: namespace.id) } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "../../../spec/lib/gitlab/import_export/", project_path: 'path') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, namespace_id: namespace.id) } context 'JSON' do let(:restored_project_json) do -- cgit v1.2.1 From 385d6df20c7f3a40b40016d5413b50690ce9a48b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 10:39:59 +0200 Subject: typo --- lib/gitlab/import_export/import_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index f7b66448abc..ea9ba9e7b4b 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -44,7 +44,7 @@ module Gitlab end def path_with_namespace - File.join(@namespace.path, @project_path) + File.join(@namespace.path, @shared.opts[:project_path]) end def repo_path -- cgit v1.2.1 From f06c5516db2c4b1df480545d11f177838abeb00d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 10:57:28 +0200 Subject: fix issue in import_service --- lib/gitlab/import_export/import_service.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index ea9ba9e7b4b..670f1ebece8 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -10,7 +10,7 @@ module Gitlab @archive_file = archive_file @current_user = owner @namespace = Namespace.find(namespace_id) - @shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace, project_path: project_path) + @shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace(project_path), project_path: project_path) end def execute @@ -43,8 +43,8 @@ module Gitlab project: ProjectWiki.new(project_tree.project)).restore end - def path_with_namespace - File.join(@namespace.path, @shared.opts[:project_path]) + def path_with_namespace(project_path) + File.join(@namespace.path, project_path) end def repo_path -- cgit v1.2.1 From ff56f7be62a94f336e4cb3382ca983cf5e2c5e54 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 11:03:55 +0200 Subject: fix importer issue --- lib/gitlab/import_export/importer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 19c5aafad20..20a93f7f6de 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -13,7 +13,7 @@ module Gitlab end def import - FileUtils.mkdir_p(@shared.storage_path) + FileUtils.mkdir_p(@shared.export_path) decompress_archive rescue => e @shared.error(e.message) @@ -23,7 +23,7 @@ module Gitlab private def decompress_archive - untar_zxf(archive: @archive_file, dir: @shared.storage_path) + untar_zxf(archive: @archive_file, dir: @shared.export_path) end end end -- cgit v1.2.1 From b2b7b38c944436af5257b1b38d612aeb3d2f237e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 11:29:06 +0200 Subject: fix rubocop warnings --- lib/gitlab/import_export/error.rb | 2 +- lib/gitlab/import_export/import_export_reader.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb index b47a646c0b9..e341c4d9cf8 100644 --- a/lib/gitlab/import_export/error.rb +++ b/lib/gitlab/import_export/error.rb @@ -2,4 +2,4 @@ module Gitlab module ImportExport class Error < StandardError; end end -end \ No newline at end of file +end diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index b7204d5a87c..a8b6294662a 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -53,7 +53,7 @@ module Gitlab parsed_hash = { include: value } @attributes_parser.parse(value) do |hash| - parsed_hash = { include: hash_or_merge(value, hash) } + parsed_hash = { include: hash_or_merge(value, hash) } end @json_config_hash[current_key] = parsed_hash end -- cgit v1.2.1 From 8165e52045711958abbb94ee9ea2c00056ccadda Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 12 May 2016 14:27:41 +0200 Subject: add author on notes to export - so we can add to a note if project member is not found --- lib/gitlab/import_export/import_export.yml | 8 ++++++-- lib/gitlab/import_export/import_export_reader.rb | 12 ++++++++++++ spec/lib/gitlab/import_export/project_tree_saver_spec.rb | 7 +++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 36128909df4..66e6ae86cfe 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -1,7 +1,8 @@ # Model relationships to be included in the project import/export project_tree: - issues: - - :notes + - notes: + :author - :labels - :milestones - :snippets @@ -10,8 +11,9 @@ project_tree: - project_members: - :user - merge_requests: + - notes: + :author - :merge_request_diff - - :notes - ci_commits: - :statuses @@ -31,6 +33,8 @@ included_attributes: - :id - :email - :username + author: + - :name # Do not include the following attributes for the models specified. excluded_attributes: diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index a8b6294662a..9aac0eae79a 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -37,10 +37,22 @@ module Gitlab @attributes_parser.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } handle_model_object(current_key, model_object) + process_sub_model(current_key, model_object) if model_object.is_a?(Hash) end @json_config_hash end + def process_sub_model(current_key, model_object) + sub_model_json = build_json_config_hash(model_object).dup + @json_config_hash.slice!(current_key) + + if @json_config_hash[current_key] && @json_config_hash[current_key][:include] + @json_config_hash[current_key][:include] << sub_model_json + else + @json_config_hash[current_key] = { include: sub_model_json } + end + end + def handle_model_object(current_key, model_object) if @json_config_hash[current_key] add_model_value(current_key, model_object) diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 67e6267b741..b9cf88d3f77 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -65,6 +65,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues'].first['notes']).not_to be_empty end + it 'has author on issue comments' do + expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty + end + it 'has project members' do expect(saved_project_json['project_members']).not_to be_empty end @@ -77,6 +81,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty end + it 'has author on merge requests comments' do + expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty + end it 'has commit statuses' do expect(saved_project_json['ci_commits'].first['statuses']).not_to be_empty end -- cgit v1.2.1 From 25a1c6541aa3dfb41ef006d42ba280d5a1d4103d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 12:33:13 +0200 Subject: add message to notes about missing author on import --- lib/gitlab/import_export/import_export_reader.rb | 2 +- lib/gitlab/import_export/importer.rb | 2 +- lib/gitlab/import_export/members_mapper.rb | 19 ++++++++++------ lib/gitlab/import_export/project_tree_restorer.rb | 26 ++++++++++++++-------- lib/gitlab/import_export/project_tree_saver.rb | 2 +- lib/gitlab/import_export/relation_factory.rb | 20 +++++++++++++++-- lib/gitlab/import_export/repo_bundler.rb | 2 +- lib/gitlab/import_export/repo_restorer.rb | 2 +- lib/gitlab/import_export/saver.rb | 2 +- lib/gitlab/import_export/shared.rb | 8 ++++--- lib/gitlab/import_export/wiki_repo_bundler.rb | 2 +- .../gitlab/import_export/members_mapper_spec.rb | 8 ++++--- .../import_export/project_tree_restorer_spec.rb | 5 +---- 13 files changed, 65 insertions(+), 35 deletions(-) diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 5d218320e23..11a736ecab6 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -15,7 +15,7 @@ module Gitlab def project_tree @attributes_parser.find_included(:project).merge(include: build_hash(@tree)) rescue => e - @shared.error(e.message) + @shared.error(e) end private diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 20a93f7f6de..d73ce43b491 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -16,7 +16,7 @@ module Gitlab FileUtils.mkdir_p(@shared.export_path) decompress_archive rescue => e - @shared.error(e.message) + @shared.error(e) false end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index d6124106f57..da8aa475653 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -2,18 +2,25 @@ module Gitlab module ImportExport class MembersMapper - def self.map(*args) - new(*args).map - end + attr_reader :map, :note_member_list def initialize(exported_members:, user:, project_id:) @exported_members = exported_members @user = user @project_id = project_id + @note_member_list = [] + + @project_member_map = Hash.new do |_, key| + @note_member_list << key + default_project_member + end + + @map = generate_map end - def map - @project_member_map = Hash.new(default_project_member) + private + + def generate_map @exported_members.each do |member| existing_user = User.where(find_project_user_query(member)).first assign_member(existing_user, member) if existing_user @@ -21,8 +28,6 @@ module Gitlab @project_member_map end - private - def assign_member(existing_user, member) old_user_id = member['user']['id'] member['user'] = existing_user diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 06bf86a1787..a840c9f9478 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -16,7 +16,7 @@ module Gitlab @project_members = @tree_hash.delete('project_members') create_relations rescue => e - @shared.error(e.message) + @shared.error(e) false end @@ -26,9 +26,10 @@ module Gitlab private - def members_map - @members ||= Gitlab::ImportExport::MembersMapper.map( - exported_members: @project_members, user: @user, project_id: project.id) + def members_mapper + @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members, + user: @user, + project_id: project.id) end def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) @@ -61,11 +62,18 @@ module Gitlab end def create_sub_relations(relation, tree_hash) - tree_hash[relation.keys.first.to_s].each do |relation_item| + relation_key = relation.keys.first.to_s + tree_hash[relation_key].each do |relation_item| relation.values.flatten.each do |sub_relation| - relation_hash = relation_item[sub_relation.to_s] - next if relation_hash.blank? - process_sub_relation(relation_hash, relation_item, sub_relation) + + if sub_relation.is_a?(Hash) + relation_hash = relation_item[sub_relation.keys.first.to_s] + sub_relation = sub_relation.keys.first + else + relation_hash = relation_item[sub_relation.to_s] + end + + process_sub_relation(relation_hash, relation_item, sub_relation) unless relation_hash.blank? end end end @@ -87,7 +95,7 @@ module Gitlab def relation_from_factory(relation, relation_hash) Gitlab::ImportExport::RelationFactory.create( - relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_map: members_map) + relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_mapper: members_mapper) end end end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 2287524c8a5..ed8ca31d936 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -14,7 +14,7 @@ module Gitlab File.write(full_path, project_json_tree) true rescue => e - @shared.error(e.message) + @shared.error(e) false end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index dd992ef443e..65e2a935fa9 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -6,11 +6,12 @@ module Gitlab OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze - def create(relation_sym:, relation_hash:, members_map:) + def create(relation_sym:, relation_hash:, members_mapper:) relation_sym = parse_relation_sym(relation_sym) klass = parse_relation(relation_hash, relation_sym) - update_user_references(relation_hash, members_map) + update_missing_author(relation_hash, members_mapper) if relation_sym == :notes + update_user_references(relation_hash, members_mapper.map) update_project_references(relation_hash, klass) imported_object(klass, relation_hash) @@ -26,6 +27,21 @@ module Gitlab end end + def update_missing_author(relation_hash, members_map) + old_author_id = relation_hash['author_id'].dup + relation_hash['author_id'] = members_map.map[old_author_id] + return unless members_map.note_member_list.include?(old_author_id) + + relation_hash['note'] = ('*Blank note*') if relation_hash['note'].blank? + relation_hash['note'].join(missing_author_note(relation_hash['updated_at'], + relation_hash['author']['name'])) + relation_hash.delete('author') + end + + def missing_author_note(updated_at, author_name) + "\n\n *By #{author_name} on #{updated_at} (imported from GitLab project)*" + end + def update_project_references(relation_hash, klass) project_id = relation_hash.delete('project_id') diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index bcf976fb624..af809f4c38c 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -22,7 +22,7 @@ module Gitlab FileUtils.mkdir_p(@shared.export_path) git_bundle(repo_path: path_to_repo, bundle_path: @full_path) rescue => e - @shared.error(e.message) + @shared.error(e) false end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 3909d447448..36094b95aa4 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -17,7 +17,7 @@ module Gitlab git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) rescue => e - @shared.error(e.message) + @shared.error(e) false end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index a2ff43a3ce3..f38229c6c59 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -20,7 +20,7 @@ module Gitlab false end rescue => e - @shared.error(e.message) + @shared.error(e) false end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 050b0428f45..01ac332981b 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -13,9 +13,11 @@ module Gitlab @export_path ||= Gitlab::ImportExport.export_path(relative_path: @opts[:relative_path]) end - def error(message) - error_out(message, caller[0].dup) - @errors << message + def error(error) + error_out(error.message, caller[0].dup) + @errors << error.message + # Debug: + Rails.logger.error(error.backtrace) end private diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb index a0000176bb5..e1e7f753720 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -12,7 +12,7 @@ module Gitlab FileUtils.mkdir_p(@shared.export_path) git_bundle(repo_path: path_to_repo, bundle_path: @full_path) rescue => e - @shared.error(e.message) + @shared.error(e) false end diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index a4d3d6d122f..78706f64fb7 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -42,9 +42,11 @@ describe Gitlab::ImportExport::MembersMapper, services: true do it 'defaults to importer project member if it does not exist' do expect(members_mapper.map[-1]).to eq(user.id) end - end - def project_member_user_id(id) - members_mapper.map[id] + it 'updates missing author IDs on missing project member' do + members_mapper.map[-1] + + expect(members_mapper.note_member_list.first).to eq(-1) + end end end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 7c6a14fc8a8..6464b0141f3 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -7,12 +7,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "../../../spec/lib/gitlab/import_export/", project_path: 'path') } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, namespace_id: namespace.id) } + let!(:restored_project_json) { project_tree_restorer.restore } context 'JSON' do - let(:restored_project_json) do - project_tree_restorer.restore - end - it 'restores models based on JSON' do expect(restored_project_json).to be true end -- cgit v1.2.1 From a86825826915f78a0728becd91f6a31df90543ea Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 14:17:10 +0200 Subject: update json and fix notes issue --- lib/gitlab/import_export/relation_factory.rb | 4 +- spec/lib/gitlab/import_export/project.json | 5747 ++++++++++++++++++++++++-- 2 files changed, 5406 insertions(+), 345 deletions(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 65e2a935fa9..0adcd0d5e6c 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -28,12 +28,12 @@ module Gitlab end def update_missing_author(relation_hash, members_map) - old_author_id = relation_hash['author_id'].dup + old_author_id = relation_hash['author_id'] relation_hash['author_id'] = members_map.map[old_author_id] return unless members_map.note_member_list.include?(old_author_id) relation_hash['note'] = ('*Blank note*') if relation_hash['note'].blank? - relation_hash['note'].join(missing_author_note(relation_hash['updated_at'], + relation_hash['note'] += (missing_author_note(relation_hash['updated_at'], relation_hash['author']['name'])) relation_hash.delete('author') end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 8b6a16c049f..31851c4c64b 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -1,429 +1,5490 @@ { - "name": "searchable_project", - "path": "gitlabhq", - "description": null, + "name": "Gitlab Test", + "path": "gitlab-test", + "description": "Aut saepe in eos dolorem aliquam hic.", "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, - "snippets_enabled": true, + "snippets_enabled": false, "visibility_level": 20, "archived": false, "issues": [ { - "id": 1, - "title": "Debitis vero omnis cum accusamus nihil rerum cupiditate.", + "id": 40, + "title": "Voluptatem modi rerum ipsum vero voluptas repudiandae veniam quibusdam.", "assignee_id": 1, - "author_id": 2, - "project_id": 6, - "created_at": "2016-04-13T14:40:32.471Z", - "updated_at": "2016-04-13T14:40:38.956Z", + "author_id": 4, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.411Z", + "updated_at": "2016-04-12T13:08:26.029Z", "position": 0, "branch_name": null, - "description": null, - "milestone_id": null, + "description": "Aut minima non sit qui nulla rerum laborum.", + "milestone_id": 10, "state": "opened", - "iid": 1, + "iid": 10, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 1357, + "note": "test", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-04-12T13:08:26.006Z", + "updated_at": "2016-04-12T13:08:26.006Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": "", + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 338, + "note": "Fugit in aliquid voluptas dolor.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.213Z", + "updated_at": "2016-03-22T15:19:59.213Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 337, + "note": "Occaecati consequatur facilis doloribus omnis hic placeat nihil.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.186Z", + "updated_at": "2016-03-22T15:19:59.186Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 336, + "note": "Nostrum et et est repudiandae non dolores voluptatem.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.156Z", + "updated_at": "2016-03-22T15:19:59.156Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 335, + "note": "Nihil et aut dolorum aut sit maxime.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.130Z", + "updated_at": "2016-03-22T15:19:59.130Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 334, + "note": "Non blanditiis voluptatem sit earum accusantium distinctio voluptas officiis.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.101Z", + "updated_at": "2016-03-22T15:19:59.101Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 333, + "note": "Nesciunt non dolorem similique nam ipsa et.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.075Z", + "updated_at": "2016-03-22T15:19:59.075Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 332, + "note": "Sed aut fugit et officiis dolor.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.047Z", + "updated_at": "2016-03-22T15:19:59.047Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 331, + "note": "Officiis iste eum recusandae suscipit consequatur consequatur.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.015Z", + "updated_at": "2016-03-22T15:19:59.015Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 40, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 39, + "title": "Sit ut adipisci sint temporibus velit quis.", + "assignee_id": 1, + "author_id": 12, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.278Z", + "updated_at": "2016-03-22T15:19:59.473Z", + "position": 0, + "branch_name": null, + "description": "Ab sint nostrum aliquam laudantium magni recusandae qui.", + "milestone_id": 10, + "state": "closed", + "iid": 9, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 346, + "note": "Natus rerum qui dolorem dolorum voluptas.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.469Z", + "updated_at": "2016-03-22T15:19:59.469Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 345, + "note": "Voluptatibus et qui quis id sed necessitatibus quos.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.438Z", + "updated_at": "2016-03-22T15:19:59.438Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 344, + "note": "Aperiam possimus ipsam quibusdam in.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.410Z", + "updated_at": "2016-03-22T15:19:59.410Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 343, + "note": "Ad vel hic molestiae tempora.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.379Z", + "updated_at": "2016-03-22T15:19:59.379Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 342, + "note": "Vel magnam sed quidem aut molestiae facilis alias.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.348Z", + "updated_at": "2016-03-22T15:19:59.348Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 341, + "note": "Veritatis dolorum aut qui quod.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.319Z", + "updated_at": "2016-03-22T15:19:59.319Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 340, + "note": "Illum at cumque dolorum et quia.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.289Z", + "updated_at": "2016-03-22T15:19:59.289Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 339, + "note": "Fugiat et error molestiae cumque quos aperiam.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.255Z", + "updated_at": "2016-03-22T15:19:59.255Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 39, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 38, + "title": "Quod quo est quis vel natus nulla eos reiciendis.", + "assignee_id": 12, + "author_id": 3, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.137Z", + "updated_at": "2016-03-22T15:19:59.712Z", + "position": 0, + "branch_name": null, + "description": "Fugit dolor accusantium suscipit facere voluptate.", + "milestone_id": 10, + "state": "opened", + "iid": 8, "updated_by_id": null, "confidential": false, "deleted_at": null, "moved_to_id": null, + "due_date": null, "notes": [ { - "id": 1, - "note": ":+1: issue", - "noteable_type": "Issue", - "author_id": 21, - "created_at": "2016-04-13T14:40:38.944Z", - "updated_at": "2016-04-13T14:40:38.944Z", - "project_id": 8, + "id": 354, + "note": "Id commodi natus vel corrupti ea placeat cum nihil.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.708Z", + "updated_at": "2016-03-22T15:19:59.708Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 353, + "note": "Quia hic sed ratione eos voluptate dolor occaecati dolorem.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.680Z", + "updated_at": "2016-03-22T15:19:59.680Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 352, + "note": "Commodi sint voluptatem est aut.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.650Z", + "updated_at": "2016-03-22T15:19:59.650Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 351, + "note": "Et quibusdam voluptatibus dolores aut quam architecto optio.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.622Z", + "updated_at": "2016-03-22T15:19:59.622Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 350, + "note": "Fugit natus explicabo sed pariatur et quasi autem.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.590Z", + "updated_at": "2016-03-22T15:19:59.590Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 349, + "note": "Corporis commodi eos quia optio sunt corrupti.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.562Z", + "updated_at": "2016-03-22T15:19:59.562Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 348, + "note": "Occaecati nostrum hic dolor tenetur aliquid maxime animi.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.536Z", + "updated_at": "2016-03-22T15:19:59.536Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 347, + "note": "Inventore ullam sed repellendus laudantium itaque et quia.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.506Z", + "updated_at": "2016-03-22T15:19:59.506Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 38, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 37, + "title": "Animi suscipit quia ut hic asperiores perferendis nisi ut.", + "assignee_id": 22, + "author_id": 10, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.994Z", + "updated_at": "2016-03-22T15:19:59.972Z", + "position": 0, + "branch_name": null, + "description": "Non quibusdam in maxime earum eveniet itaque culpa.", + "milestone_id": 11, + "state": "closed", + "iid": 7, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 362, + "note": "Quia qui quis molestiae in praesentium.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:19:59.966Z", + "updated_at": "2016-03-22T15:19:59.966Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 361, + "note": "Maxime sed eius qui consequatur beatae.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:19:59.924Z", + "updated_at": "2016-03-22T15:19:59.924Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 360, + "note": "Voluptatum quasi corrupti eveniet sed ut quis quibusdam.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:19:59.897Z", + "updated_at": "2016-03-22T15:19:59.897Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 359, + "note": "Molestias quia eius ipsum non.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:19:59.866Z", + "updated_at": "2016-03-22T15:19:59.866Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 358, + "note": "Aut non est accusantium aliquam.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:19:59.834Z", + "updated_at": "2016-03-22T15:19:59.834Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 357, + "note": "Aspernatur voluptas id voluptas vel cum ipsam.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:19:59.805Z", + "updated_at": "2016-03-22T15:19:59.805Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 356, + "note": "Harum dignissimos provident tempora sit numquam est qui.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:19:59.773Z", + "updated_at": "2016-03-22T15:19:59.773Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 355, + "note": "Sint dignissimos molestiae recusandae delectus.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:19:59.746Z", + "updated_at": "2016-03-22T15:19:59.746Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 37, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 36, + "title": "Quia dolores commodi eligendi ut nemo totam.", + "assignee_id": 3, + "author_id": 4, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.814Z", + "updated_at": "2016-03-22T15:20:00.371Z", + "position": 0, + "branch_name": null, + "description": "Molestiae veniam laudantium autem et natus.", + "milestone_id": 11, + "state": "opened", + "iid": 6, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 370, + "note": "Occaecati temporibus tempore harum vero incidunt veniam iste.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:00.365Z", + "updated_at": "2016-03-22T15:20:00.365Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 369, + "note": "Modi architecto officiis quia iste voluptas libero nihil quo.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:00.331Z", + "updated_at": "2016-03-22T15:20:00.331Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 368, + "note": "Eaque est tenetur ex est molestiae nobis.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:00.296Z", + "updated_at": "2016-03-22T15:20:00.296Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 367, + "note": "Odit enim ut a quo qui.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:00.261Z", + "updated_at": "2016-03-22T15:20:00.261Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 366, + "note": "Omnis unde cum officiis est.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:00.223Z", + "updated_at": "2016-03-22T15:20:00.223Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 365, + "note": "Ab consequuntur aliquam illo voluptatum.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:00.178Z", + "updated_at": "2016-03-22T15:20:00.178Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 364, + "note": "Molestiae dolorem est eos dolores aut.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:00.127Z", + "updated_at": "2016-03-22T15:20:00.127Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 363, + "note": "Nemo velit nam quod veniam.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:00.083Z", + "updated_at": "2016-03-22T15:20:00.083Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 36, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 35, + "title": "Rerum tenetur harum molestiae quam aut praesentium quaerat doloremque.", + "assignee_id": 4, + "author_id": 1, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.660Z", + "updated_at": "2016-03-22T15:20:00.665Z", + "position": 0, + "branch_name": null, + "description": "Omnis et voluptatibus expedita qui et explicabo rem ut.", + "milestone_id": 11, + "state": "opened", + "iid": 5, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 378, + "note": "Molestiae atque exercitationem culpa harum nemo.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:00.660Z", + "updated_at": "2016-03-22T15:20:00.660Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 377, + "note": "Porro sed nobis neque amet velit velit.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:00.625Z", + "updated_at": "2016-03-22T15:20:00.625Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 376, + "note": "Dicta officiis doloremque voluptatum qui omnis.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:00.589Z", + "updated_at": "2016-03-22T15:20:00.589Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 375, + "note": "Incidunt rerum omnis cum laudantium aut impedit.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:00.553Z", + "updated_at": "2016-03-22T15:20:00.553Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 374, + "note": "Et suscipit omnis dolorum officia vero.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:00.517Z", + "updated_at": "2016-03-22T15:20:00.517Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 373, + "note": "Doloremque adipisci et cumque inventore beatae consectetur.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:00.485Z", + "updated_at": "2016-03-22T15:20:00.485Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 372, + "note": "Dolores sapiente ea dolorum et quae adipisci id.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:00.455Z", + "updated_at": "2016-03-22T15:20:00.455Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 371, + "note": "Accusantium repellat tenetur natus dicta ullam saepe facere.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:00.420Z", + "updated_at": "2016-03-22T15:20:00.420Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 35, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 34, + "title": "Enim occaecati aut sed quia mollitia eligendi atque dolores voluptatem.", + "assignee_id": 24, + "author_id": 1, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.506Z", + "updated_at": "2016-03-22T15:20:00.961Z", + "position": 0, + "branch_name": null, + "description": "Voluptatem totam magnam fugit assumenda consequatur illo qui.", + "milestone_id": 10, + "state": "opened", + "iid": 4, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 379, + "note": "Praesentium odio quia fugit consequuntur repudiandae ducimus.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:00.717Z", + "updated_at": "2016-03-22T15:20:00.717Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + }, + { + "id": 380, + "note": "Dolores aut dolorem quia soluta incidunt commodi quia.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:00.754Z", + "updated_at": "2016-03-22T15:20:00.754Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 381, + "note": "Enim et velit iure ad.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:00.787Z", + "updated_at": "2016-03-22T15:20:00.787Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 382, + "note": "Impedit nobis quis laudantium ad assumenda.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:00.822Z", + "updated_at": "2016-03-22T15:20:00.822Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 383, + "note": "Facere sed numquam quos quas.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:00.855Z", + "updated_at": "2016-03-22T15:20:00.855Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 384, + "note": "Ex voluptatem sit provident error.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:00.889Z", + "updated_at": "2016-03-22T15:20:00.889Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 385, + "note": "Soluta laboriosam recusandae est cupiditate.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:00.925Z", + "updated_at": "2016-03-22T15:20:00.925Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 386, + "note": "Similique dolorem rerum iusto animi perferendis aut inventore.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:00.957Z", + "updated_at": "2016-03-22T15:20:00.957Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 34, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + } + ] + }, + { + "id": 33, + "title": "Rem fugiat fugit occaecati quibusdam enim consectetur numquam.", + "assignee_id": 22, + "author_id": 22, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.364Z", + "updated_at": "2016-03-22T15:20:01.227Z", + "position": 0, + "branch_name": null, + "description": "Provident nulla architecto neque beatae fuga alias repudiandae.", + "milestone_id": 10, + "state": "closed", + "iid": 3, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 394, + "note": "Suscipit numquam voluptatibus ipsam libero dolorum dolore totam.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:01.223Z", + "updated_at": "2016-03-22T15:20:01.223Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 393, + "note": "Et et sed sit sint.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:01.194Z", + "updated_at": "2016-03-22T15:20:01.194Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 392, + "note": "Corrupti perferendis voluptas et iure omnis officia.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:01.160Z", + "updated_at": "2016-03-22T15:20:01.160Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 391, + "note": "Autem quo fugit in iste nesciunt tempora.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:01.131Z", + "updated_at": "2016-03-22T15:20:01.131Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 390, + "note": "Magni porro ut soluta quis et eveniet maiores.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:01.101Z", + "updated_at": "2016-03-22T15:20:01.101Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 389, + "note": "Sed consequuntur debitis nisi veniam exercitationem recusandae a quisquam.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:01.070Z", + "updated_at": "2016-03-22T15:20:01.070Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 388, + "note": "Aut impedit qui consectetur dicta temporibus.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:01.042Z", + "updated_at": "2016-03-22T15:20:01.042Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 387, + "note": "Officia repudiandae ut culpa ipsa reiciendis.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:01.005Z", + "updated_at": "2016-03-22T15:20:01.005Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 33, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 32, + "title": "Velit nihil est alias blanditiis eius earum autem hic.", + "assignee_id": 22, + "author_id": 26, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.225Z", + "updated_at": "2016-03-22T15:20:01.495Z", + "position": 0, + "branch_name": null, + "description": "Id voluptas ut sint aut laborum nobis commodi.", + "milestone_id": 11, + "state": "opened", + "iid": 2, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 402, + "note": "Magni ut eligendi sit sint recusandae voluptas tempore necessitatibus.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:01.489Z", + "updated_at": "2016-03-22T15:20:01.489Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 401, + "note": "Est repellat commodi incidunt tempore earum optio unde sint.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:01.455Z", + "updated_at": "2016-03-22T15:20:01.455Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 400, + "note": "Vero unde debitis tempore est laboriosam ut esse.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:01.421Z", + "updated_at": "2016-03-22T15:20:01.421Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 399, + "note": "Omnis qui asperiores expedita harum voluptatem eius.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:01.391Z", + "updated_at": "2016-03-22T15:20:01.391Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 398, + "note": "Dolorem doloribus delectus quo ratione esse veritatis.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:01.358Z", + "updated_at": "2016-03-22T15:20:01.358Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 397, + "note": "Quia esse et odit id est omnis dolorum quia.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:01.329Z", + "updated_at": "2016-03-22T15:20:01.329Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 396, + "note": "Exercitationem suscipit non rerum tempore sit.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:01.297Z", + "updated_at": "2016-03-22T15:20:01.297Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 395, + "note": "Nihil veniam magni sit officiis.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:01.268Z", + "updated_at": "2016-03-22T15:20:01.268Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 32, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + }, + { + "id": 31, + "title": "Asperiores recusandae praesentium voluptas pariatur provident qui exercitationem quis.", + "assignee_id": 26, + "author_id": 24, + "project_id": 5, + "created_at": "2016-03-22T15:13:26.889Z", + "updated_at": "2016-03-22T15:20:01.834Z", + "position": 0, + "branch_name": null, + "description": "Ex voluptates qui excepturi cupiditate.", + "milestone_id": 11, + "state": "closed", + "iid": 1, + "updated_by_id": null, + "confidential": false, + "deleted_at": null, + "moved_to_id": null, + "due_date": null, + "notes": [ + { + "id": 410, + "note": "Sit itaque non nihil nisi qui voluptatem dolorem error.", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2016-03-22T15:20:01.828Z", + "updated_at": "2016-03-22T15:20:01.828Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 409, + "note": "Omnis rem nihil molestiae enim laudantium doloremque.", + "noteable_type": "Issue", + "author_id": 3, + "created_at": "2016-03-22T15:20:01.783Z", + "updated_at": "2016-03-22T15:20:01.783Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 408, + "note": "Ullam harum sit et optio incidunt.", + "noteable_type": "Issue", + "author_id": 4, + "created_at": "2016-03-22T15:20:01.746Z", + "updated_at": "2016-03-22T15:20:01.746Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 407, + "note": "Fugit distinctio ab quo ipsam.", + "noteable_type": "Issue", + "author_id": 10, + "created_at": "2016-03-22T15:20:01.716Z", + "updated_at": "2016-03-22T15:20:01.716Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 406, + "note": "Impedit iste possimus ad ea.", + "noteable_type": "Issue", + "author_id": 12, + "created_at": "2016-03-22T15:20:01.676Z", + "updated_at": "2016-03-22T15:20:01.676Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 405, + "note": "Nemo recusandae dolore distinctio quam consequuntur ut et aut.", + "noteable_type": "Issue", + "author_id": 22, + "created_at": "2016-03-22T15:20:01.641Z", + "updated_at": "2016-03-22T15:20:01.641Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 404, + "note": "Nisi repudiandae repellat nulla culpa quasi expedita quod velit.", + "noteable_type": "Issue", + "author_id": 24, + "created_at": "2016-03-22T15:20:01.601Z", + "updated_at": "2016-03-22T15:20:01.601Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 403, + "note": "Quibusdam odio temporibus nemo voluptatibus accusamus.", + "noteable_type": "Issue", + "author_id": 26, + "created_at": "2016-03-22T15:20:01.552Z", + "updated_at": "2016-03-22T15:20:01.552Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 31, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ] + } + ], + "labels": [ + { + "id": 12, + "title": "test", + "color": "#428bca", + "project_id": 5, + "created_at": "2016-05-10T10:53:14.214Z", + "updated_at": "2016-05-10T10:53:14.214Z", + "template": false, + "description": "test label" + } + ], + "milestones": [ + { + "id": 11, + "title": "v2.0", + "project_id": 5, + "description": "Sapiente facilis architecto reprehenderit aut sed enim.", + "due_date": null, + "created_at": "2016-03-22T15:13:21.631Z", + "updated_at": "2016-03-22T15:13:21.631Z", + "state": "closed", + "iid": 2 + }, + { + "id": 10, + "title": "v1.0", + "project_id": 5, + "description": "Est sed eos minima veniam culpa aut non.", + "due_date": null, + "created_at": "2016-03-22T15:13:21.622Z", + "updated_at": "2016-03-22T15:13:21.622Z", + "state": "closed", + "iid": 1 + } + ], + "snippets": [ + + ], + "releases": [ + + ], + "events": [ + { + "id": 301, + "target_type": "Note", + "target_id": 1357, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-04-12T13:08:30.886Z", + "updated_at": "2016-04-12T13:08:30.886Z", + "action": 6, + "author_id": 1 + }, + { + "id": 227, + "target_type": "MergeRequest", + "target_id": 85, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:19:44.957Z", + "updated_at": "2016-03-22T15:19:44.957Z", + "action": 1, + "author_id": 1 + }, + { + "id": 226, + "target_type": "MergeRequest", + "target_id": 84, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:19:44.600Z", + "updated_at": "2016-03-22T15:19:44.600Z", + "action": 1, + "author_id": 1 + }, + { + "id": 157, + "target_type": "MergeRequest", + "target_id": 15, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:45.936Z", + "updated_at": "2016-03-22T15:13:45.936Z", + "action": 1, + "author_id": 3 + }, + { + "id": 156, + "target_type": "MergeRequest", + "target_id": 14, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:45.500Z", + "updated_at": "2016-03-22T15:13:45.500Z", + "action": 1, + "author_id": 10 + }, + { + "id": 155, + "target_type": "MergeRequest", + "target_id": 13, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:45.242Z", + "updated_at": "2016-03-22T15:13:45.242Z", + "action": 1, + "author_id": 1 + }, + { + "id": 154, + "target_type": "MergeRequest", + "target_id": 12, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:44.940Z", + "updated_at": "2016-03-22T15:13:44.940Z", + "action": 1, + "author_id": 24 + }, + { + "id": 153, + "target_type": "MergeRequest", + "target_id": 11, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:44.568Z", + "updated_at": "2016-03-22T15:13:44.568Z", + "action": 1, + "author_id": 26 + }, + { + "id": 152, + "target_type": "MergeRequest", + "target_id": 10, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:44.225Z", + "updated_at": "2016-03-22T15:13:44.225Z", + "action": 1, + "author_id": 22 + }, + { + "id": 151, + "target_type": "MergeRequest", + "target_id": 9, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:43.868Z", + "updated_at": "2016-03-22T15:13:43.868Z", + "action": 1, + "author_id": 24 + }, + { + "id": 102, + "target_type": "Issue", + "target_id": 40, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.474Z", + "updated_at": "2016-03-22T15:13:28.474Z", + "action": 1, + "author_id": 4 + }, + { + "id": 101, + "target_type": "Issue", + "target_id": 39, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.328Z", + "updated_at": "2016-03-22T15:13:28.328Z", + "action": 1, + "author_id": 12 + }, + { + "id": 100, + "target_type": "Issue", + "target_id": 38, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.204Z", + "updated_at": "2016-03-22T15:13:28.204Z", + "action": 1, + "author_id": 3 + }, + { + "id": 99, + "target_type": "Issue", + "target_id": 37, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:28.055Z", + "updated_at": "2016-03-22T15:13:28.055Z", + "action": 1, + "author_id": 10 + }, + { + "id": 98, + "target_type": "Issue", + "target_id": 36, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.913Z", + "updated_at": "2016-03-22T15:13:27.913Z", + "action": 1, + "author_id": 4 + }, + { + "id": 97, + "target_type": "Issue", + "target_id": 35, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.731Z", + "updated_at": "2016-03-22T15:13:27.731Z", + "action": 1, + "author_id": 1 + }, + { + "id": 96, + "target_type": "Issue", + "target_id": 34, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.564Z", + "updated_at": "2016-03-22T15:13:27.564Z", + "action": 1, + "author_id": 1 + }, + { + "id": 95, + "target_type": "Issue", + "target_id": 33, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.429Z", + "updated_at": "2016-03-22T15:13:27.429Z", + "action": 1, + "author_id": 22 + }, + { + "id": 94, + "target_type": "Issue", + "target_id": 32, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:27.287Z", + "updated_at": "2016-03-22T15:13:27.287Z", + "action": 1, + "author_id": 26 + }, + { + "id": 93, + "target_type": "Issue", + "target_id": 31, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:26.997Z", + "updated_at": "2016-03-22T15:13:26.997Z", + "action": 1, + "author_id": 24 + }, + { + "id": 51, + "target_type": "Milestone", + "target_id": 11, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:21.634Z", + "updated_at": "2016-03-22T15:13:21.634Z", + "action": 1, + "author_id": 26 + }, + { + "id": 50, + "target_type": "Milestone", + "target_id": 10, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:21.625Z", + "updated_at": "2016-03-22T15:13:21.625Z", + "action": 1, + "author_id": 22 + }, + { + "id": 24, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.750Z", + "updated_at": "2016-03-22T15:13:20.750Z", + "action": 8, + "author_id": 12 + }, + { + "id": 23, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.711Z", + "updated_at": "2016-03-22T15:13:20.711Z", + "action": 8, + "author_id": 22 + }, + { + "id": 22, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.667Z", + "updated_at": "2016-03-22T15:13:20.667Z", + "action": 8, + "author_id": 26 + }, + { + "id": 21, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:20.646Z", + "updated_at": "2016-03-22T15:13:20.646Z", + "action": 8, + "author_id": 1 + }, + { + "id": 5, + "target_type": null, + "target_id": null, + "title": null, + "data": null, + "project_id": 5, + "created_at": "2016-03-22T15:13:10.369Z", + "updated_at": "2016-03-22T15:13:10.369Z", + "action": 1, + "author_id": 1 + } + ], + "project_members": [ + { + "id": 35, + "access_level": 40, + "source_id": 5, + "source_type": "Project", + "user_id": 12, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.743Z", + "updated_at": "2016-03-22T15:13:20.743Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 12, + "email": "maureen.bogisich@russelkessler.com", + "username": "evans" + } + }, + { + "id": 34, + "access_level": 40, + "source_id": 5, + "source_type": "Project", + "user_id": 22, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.708Z", + "updated_at": "2016-03-22T15:13:20.708Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 22, + "email": "user0@example.com", + "username": "user0" + } + }, + { + "id": 33, + "access_level": 40, + "source_id": 5, + "source_type": "Project", + "user_id": 26, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.664Z", + "updated_at": "2016-03-22T15:13:20.664Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 26, + "email": "user4@example.com", + "username": "user4" + } + }, + { + "id": 32, + "access_level": 20, + "source_id": 5, + "source_type": "Project", + "user_id": 1, + "notification_level": 3, + "created_at": "2016-03-22T15:13:20.643Z", + "updated_at": "2016-03-22T15:13:20.643Z", + "created_by_id": null, + "invite_email": null, + "invite_token": null, + "invite_accepted_at": null, + "user": { + "id": 1, + "email": "nospam@bluegod.net", + "username": "root" + } + } + ], + "merge_requests": [ + { + "id": 85, + "target_branch": "feature", + "source_branch": "feature_conflict", + "source_project_id": 5, + "author_id": 1, + "assignee_id": null, + "title": "Cannot be automatically merged", + "created_at": "2016-03-22T15:19:44.807Z", + "updated_at": "2016-03-22T15:20:09.557Z", + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 9, + "description": null, + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 638, + "note": "Ab velit ducimus totam sunt ut.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:09.553Z", + "updated_at": "2016-03-22T15:20:09.553Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 637, + "note": "Ipsum aliquam est in unde similique nihil illo ea.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:09.528Z", + "updated_at": "2016-03-22T15:20:09.528Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 636, + "note": "Soluta inventore adipisci et consequatur expedita aliquid earum modi.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:09.496Z", + "updated_at": "2016-03-22T15:20:09.496Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 635, + "note": "Corporis incidunt tempore est deleniti.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:09.469Z", + "updated_at": "2016-03-22T15:20:09.469Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 634, + "note": "Hic dolores voluptatibus qui necessitatibus.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:09.440Z", + "updated_at": "2016-03-22T15:20:09.440Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 633, + "note": "Rerum architecto placeat doloribus voluptates consequuntur quo.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:09.412Z", + "updated_at": "2016-03-22T15:20:09.412Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 632, + "note": "Vel earum aut ut occaecati aut ut rerum qui.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:09.389Z", + "updated_at": "2016-03-22T15:20:09.389Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 631, + "note": "Est voluptatibus dolores animi numquam.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:09.361Z", + "updated_at": "2016-03-22T15:20:09.361Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 85, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 85, + "state": "collected", + "st_commits": [ + { + "id": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc", + "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "5937ac0a7beb003549fc5fd26fc247adbce4a52e" + ], + "authored_date": "2014-08-06T08:35:52.000+02:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-08-06T08:35:52.000+02:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + ], + "authored_date": "2014-02-27T10:01:38.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T10:01:38.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + ], + "authored_date": "2014-02-27T09:57:31.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:57:31.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "d14d6c0abdd253381df51a723d58691b2ee1ab08" + ], + "authored_date": "2014-02-27T09:54:21.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:54:21.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" + ], + "authored_date": "2014-02-27T09:49:50.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:49:50.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + }, + { + "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:48:32.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:48:32.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "new_path": ".DS_Store", + "old_path": ".DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "new_path": ".gitignore", + "old_path": ".gitignore", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "new_path": ".gitmodules", + "old_path": ".gitmodules", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "new_path": "files/.DS_Store", + "old_path": "files/.DS_Store", + "a_mode": "100644", + "b_mode": "0", + "new_file": false, + "renamed_file": false, + "deleted_file": true, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n", + "new_path": "files/ruby/feature.rb", + "old_path": "files/ruby/feature.rb", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "new_path": "files/ruby/popen.rb", + "old_path": "files/ruby/popen.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "new_path": "files/ruby/regex.rb", + "old_path": "files/ruby/regex.rb", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "new_path": "gitlab-grack", + "old_path": "gitlab-grack", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "new_path": "gitlab-shell", + "old_path": "gitlab-shell", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 85, + "created_at": "2016-03-22T15:19:44.810Z", + "updated_at": "2016-03-22T15:19:44.901Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "9" + } + }, + { + "id": 84, + "target_branch": "master", + "source_branch": "feature", + "source_project_id": 5, + "author_id": 1, + "assignee_id": null, + "title": "Can be automatically merged", + "created_at": "2016-03-22T15:19:44.482Z", + "updated_at": "2016-03-22T15:20:09.773Z", + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 8, + "description": null, + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 646, + "note": "Temporibus debitis veniam est ut sit nihil.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:09.770Z", + "updated_at": "2016-03-22T15:20:09.770Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 645, + "note": "Ut assumenda dignissimos quibusdam veritatis sequi dolores.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:09.740Z", + "updated_at": "2016-03-22T15:20:09.740Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 644, + "note": "Velit quae quidem cupiditate laudantium nihil ut eveniet.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:09.717Z", + "updated_at": "2016-03-22T15:20:09.717Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 643, + "note": "Repellat quas porro sed mollitia laborum ut fugiat.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:09.690Z", + "updated_at": "2016-03-22T15:20:09.690Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 642, + "note": "Qui aut debitis perspiciatis et voluptatem.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:09.665Z", + "updated_at": "2016-03-22T15:20:09.665Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 641, + "note": "Quia id quia velit et.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:09.639Z", + "updated_at": "2016-03-22T15:20:09.639Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 640, + "note": "Corporis commodi doloremque itaque non animi.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:09.617Z", + "updated_at": "2016-03-22T15:20:09.617Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 639, + "note": "Possimus dignissimos voluptatum in tenetur.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:09.589Z", + "updated_at": "2016-03-22T15:20:09.589Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 84, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 84, + "state": "collected", + "st_commits": [ + { + "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9", + "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "parent_ids": [ + "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" + ], + "authored_date": "2014-02-27T09:26:01.000+01:00", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T09:26:01.000+01:00", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n", + "new_path": "files/ruby/feature.rb", + "old_path": "files/ruby/feature.rb", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 84, + "created_at": "2016-03-22T15:19:44.485Z", + "updated_at": "2016-03-22T15:19:44.577Z", + "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "real_size": "1" + } + }, + { + "id": 15, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 3, + "assignee_id": 3, + "title": "Nulla explicabo iure voluptas perferendis autem autem unde nemo totam optio.", + "created_at": "2016-03-22T15:13:45.689Z", + "updated_at": "2016-03-22T15:20:30.476Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 7, + "description": "Doloribus dignissimos impedit qui et provident exercitationem. Veniam quis magni qui fugiat. Et quia voluptate et vel consequatur pariatur ea est.", + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 1231, + "note": "Rerum optio quibusdam provident possimus quis cum.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:30.472Z", + "updated_at": "2016-03-22T15:20:30.472Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 1230, + "note": "Quasi odit repudiandae ut officiis ut nihil illo.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:30.444Z", + "updated_at": "2016-03-22T15:20:30.444Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1229, + "note": "Aut vero dolores facere sed.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:30.412Z", + "updated_at": "2016-03-22T15:20:30.412Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1228, + "note": "Autem voluptatem et blanditiis accusantium deserunt et et.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:30.383Z", + "updated_at": "2016-03-22T15:20:30.383Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1227, + "note": "Voluptatem aliquam voluptatem molestiae est.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:30.352Z", + "updated_at": "2016-03-22T15:20:30.352Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1226, + "note": "Ea aut cupiditate est consequatur animi error qui et.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:30.319Z", + "updated_at": "2016-03-22T15:20:30.319Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 1225, + "note": "Voluptates est voluptas et nostrum modi beatae inventore et.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:30.289Z", + "updated_at": "2016-03-22T15:20:30.289Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1224, + "note": "Quia est rerum adipisci cupiditate.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:30.260Z", + "updated_at": "2016-03-22T15:20:30.260Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 15, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 15, + "state": "collected", + "st_commits": [ + { + "id": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6", + "parent_ids": [ + "5f923865dde3436854e9ceb9cdb7815618d4e849", + "048721d90c449b244b7b4c53a9186b04330174ec" + ], + "authored_date": "2015-12-07T12:52:12.000+01:00", + "author_name": "Marin Jankovski", + "author_email": "marin@gitlab.com", + "committed_date": "2015-12-07T12:52:12.000+01:00", + "committer_name": "Marin Jankovski", + "committer_email": "marin@gitlab.com" + }, + { + "id": "048721d90c449b244b7b4c53a9186b04330174ec", + "message": "LFS object pointer.\n", + "parent_ids": [ + "5f923865dde3436854e9ceb9cdb7815618d4e849" + ], + "authored_date": "2015-12-07T11:54:28.000+01:00", + "author_name": "Marin Jankovski", + "author_email": "maxlazio@gmail.com", + "committed_date": "2015-12-07T11:54:28.000+01:00", + "committer_name": "Marin Jankovski", + "committer_email": "maxlazio@gmail.com" + }, + { + "id": "5f923865dde3436854e9ceb9cdb7815618d4e849", + "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n", + "parent_ids": [ + "d2d430676773caa88cdaf7c55944073b2fd5561a" + ], + "authored_date": "2015-11-13T16:27:12.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T16:27:12.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "d2d430676773caa88cdaf7c55944073b2fd5561a", + "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5", + "parent_ids": [ + "59e29889be61e6e0e5e223bfa9ac2721d31605b8", + "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73" + ], + "authored_date": "2015-11-13T08:50:17.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T08:50:17.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73", + "message": "Add GitLab SVG\n", + "parent_ids": [ + "59e29889be61e6e0e5e223bfa9ac2721d31605b8" + ], + "authored_date": "2015-11-13T08:39:43.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T08:39:43.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8", + "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4", + "parent_ids": [ + "19e2e9b4ef76b422ce1154af39a91323ccc57434", + "66eceea0db202bb39c4e445e8ca28689645366c5" + ], + "authored_date": "2015-11-13T07:21:40.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T07:21:40.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "66eceea0db202bb39c4e445e8ca28689645366c5", + "message": "add spaces in whitespace file\n", + "parent_ids": [ + "08f22f255f082689c0d7d39d19205085311542bc" + ], + "authored_date": "2015-11-13T06:01:27.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T06:01:27.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "08f22f255f082689c0d7d39d19205085311542bc", + "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n", + "parent_ids": [ + "c642fe9b8b9f28f9225d7ea953fe14e74748d53b" + ], + "authored_date": "2015-11-13T06:00:16.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T06:00:16.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434", + "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3", + "parent_ids": [ + "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd", + "c642fe9b8b9f28f9225d7ea953fe14e74748d53b" + ], + "authored_date": "2015-11-13T05:23:14.000+01:00", + "author_name": "Stan Hu", + "author_email": "stanhu@gmail.com", + "committed_date": "2015-11-13T05:23:14.000+01:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@gmail.com" + }, + { + "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b", + "message": "add whitespace in empty\n", + "parent_ids": [ + "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0" + ], + "authored_date": "2015-11-13T05:08:45.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T05:08:45.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0", + "message": "add empty file\n", + "parent_ids": [ + "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd" + ], + "authored_date": "2015-11-13T05:08:04.000+01:00", + "author_name": "윤민식", + "author_email": "minsik.yoon@samsung.com", + "committed_date": "2015-11-13T05:08:04.000+01:00", + "committer_name": "윤민식", + "committer_email": "minsik.yoon@samsung.com" + }, + { + "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd", + "message": "Add ISO-8859 test file\n", + "parent_ids": [ + "e56497bb5f03a90a51293fc6d516788730953899" + ], + "authored_date": "2015-08-25T17:53:12.000+02:00", + "author_name": "Stan Hu", + "author_email": "stanhu@packetzoom.com", + "committed_date": "2015-08-25T17:53:12.000+02:00", + "committer_name": "Stan Hu", + "committer_email": "stanhu@packetzoom.com" + }, + { + "id": "e56497bb5f03a90a51293fc6d516788730953899", + "message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n", + "parent_ids": [ + "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "4cd80ccab63c82b4bad16faa5193fbd2aa06df40" + ], + "authored_date": "2015-01-10T22:23:29.000+01:00", + "author_name": "Sytse Sijbrandij", + "author_email": "sytse@gitlab.com", + "committed_date": "2015-01-10T22:23:29.000+01:00", + "committer_name": "Sytse Sijbrandij", + "committer_email": "sytse@gitlab.com" + }, + { + "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40", + "message": "add directory structure for tree_helper spec\n", + "parent_ids": [ + "5937ac0a7beb003549fc5fd26fc247adbce4a52e" + ], + "authored_date": "2015-01-10T21:28:18.000+01:00", + "author_name": "marmis85", + "author_email": "marmis85@gmail.com", + "committed_date": "2015-01-10T21:28:18.000+01:00", + "committer_name": "marmis85", + "committer_email": "marmis85@gmail.com" + } + ], + "st_diffs": [ + { + "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n", + "new_path": "CHANGELOG", + "old_path": "CHANGELOG", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n", + "new_path": "encoding/iso8859.txt", + "old_path": "encoding/iso8859.txt", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "new_path": "files/images/wm.svg", + "old_path": "files/images/wm.svg", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n", + "new_path": "files/lfs/lfs_object.iso", + "old_path": "files/lfs/lfs_object.iso", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n", + "new_path": "files/whitespace", + "old_path": "files/whitespace", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + }, + { + "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n", + "new_path": "foo/bar/.gitkeep", + "old_path": "foo/bar/.gitkeep", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 15, + "created_at": "2016-03-22T15:13:45.692Z", + "updated_at": "2016-03-22T15:13:45.808Z", + "base_commit_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "real_size": "6" + } + }, + { + "id": 14, + "target_branch": "test-1", + "source_branch": "test-10", + "source_project_id": 5, + "author_id": 10, + "assignee_id": 1, + "title": "Tempore aliquid sit amet odit qui cum iusto voluptatibus asperiores.", + "created_at": "2016-03-22T15:13:45.442Z", + "updated_at": "2016-03-22T15:20:30.735Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 6, + "description": "Quis et et autem saepe ut. Eum corporis tempore cum dolore. Molestiae pariatur voluptatem officia perferendis aut veniam.", + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 1239, + "note": "Aspernatur suscipit veritatis aliquid rerum.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:30.731Z", + "updated_at": "2016-03-22T15:20:30.731Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 1238, + "note": "Rerum deleniti omnis porro commodi.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:30.701Z", + "updated_at": "2016-03-22T15:20:30.701Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1237, + "note": "Eaque ut magnam rerum non dolores esse.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:30.667Z", + "updated_at": "2016-03-22T15:20:30.667Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1236, + "note": "Fugit et aut similique illum ut natus maiores et.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:30.637Z", + "updated_at": "2016-03-22T15:20:30.637Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1235, + "note": "Qui qui temporibus eos aliquam.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:30.608Z", + "updated_at": "2016-03-22T15:20:30.608Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1234, + "note": "Voluptates hic dolorum aut inventore.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:30.575Z", + "updated_at": "2016-03-22T15:20:30.575Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 1233, + "note": "Dolorum iure at dolor dolores numquam iusto.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:30.548Z", + "updated_at": "2016-03-22T15:20:30.548Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1232, + "note": "Nihil est eum aspernatur amet minus et corporis consectetur.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:30.517Z", + "updated_at": "2016-03-22T15:20:30.517Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 14, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 14, + "state": "collected", + "st_commits": [ + { + "id": "bce96ecee98f51fa5d91021e6c42859a35a701ad", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:40:05.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:40:05.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 14, + "created_at": "2016-03-22T15:13:45.444Z", + "updated_at": "2016-03-22T15:13:45.486Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 13, + "target_branch": "test-11", + "source_branch": "test-12", + "source_project_id": 5, + "author_id": 1, + "assignee_id": 26, + "title": "Voluptas minus sunt voluptatum quis quia ut velit distinctio itaque.", + "created_at": "2016-03-22T15:13:45.164Z", + "updated_at": "2016-03-22T15:20:30.994Z", + "milestone_id": 11, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 5, + "description": "Ea ut modi consectetur et minus beatae. Et sunt ducimus praesentium libero officia maiores voluptas cumque. Rerum in aut corporis et ullam omnis.", + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 1247, + "note": "Non error magnam placeat cupiditate eum.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:30.989Z", + "updated_at": "2016-03-22T15:20:30.989Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 1246, + "note": "Eos optio et architecto eligendi ea est nihil.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:30.957Z", + "updated_at": "2016-03-22T15:20:30.957Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1245, + "note": "Reprehenderit in atque dolor et repudiandae a est.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:30.928Z", + "updated_at": "2016-03-22T15:20:30.928Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1244, + "note": "Numquam fugit doloremque iure odio et.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:30.902Z", + "updated_at": "2016-03-22T15:20:30.902Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1243, + "note": "Doloribus laboriosam id harum voluptatum vitae ut quam.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:30.863Z", + "updated_at": "2016-03-22T15:20:30.863Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1242, + "note": "Harum et ut ipsum dolore ea.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:30.832Z", + "updated_at": "2016-03-22T15:20:30.832Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 1241, + "note": "Corporis sed soluta ut est modi natus ab.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:30.802Z", + "updated_at": "2016-03-22T15:20:30.802Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1240, + "note": "Corrupti totam tenetur officiis ratione dolores est qui vel.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:30.771Z", + "updated_at": "2016-03-22T15:20:30.771Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 13, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 13, + "state": "collected", + "st_commits": [ + { + "id": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:44:02.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:44:02.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 13, + "created_at": "2016-03-22T15:13:45.167Z", + "updated_at": "2016-03-22T15:13:45.216Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 12, + "target_branch": "test-15", + "source_branch": "test-2", + "source_project_id": 5, + "author_id": 24, + "assignee_id": 12, + "title": "In assumenda nam quaerat qui eos sit facilis enim quia quis.", + "created_at": "2016-03-22T15:13:44.837Z", + "updated_at": "2016-03-22T15:20:31.258Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 4, + "description": "Soluta excepturi quis iste vero delectus rerum. Consequatur possimus aliquam necessitatibus deleniti rerum est impedit. Eius rem et consequatur assumenda est commodi.", + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 1255, + "note": "Quibusdam rem aut similique ipsum recusandae ut accusamus.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:31.253Z", + "updated_at": "2016-03-22T15:20:31.253Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 1254, + "note": "Cumque sed omnis ipsa et magnam dolorem et.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:31.224Z", + "updated_at": "2016-03-22T15:20:31.224Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1253, + "note": "Molestiae beatae id consequatur nam minus quia.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:31.195Z", + "updated_at": "2016-03-22T15:20:31.195Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1252, + "note": "Voluptatem dolorem dignissimos itaque tempora quas ut.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:31.166Z", + "updated_at": "2016-03-22T15:20:31.166Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1251, + "note": "Debitis qui quibusdam voluptas repellat veritatis dicta rerum id.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:31.137Z", + "updated_at": "2016-03-22T15:20:31.137Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1250, + "note": "Suscipit optio ad voluptatem dignissimos temporibus amet molestias ut.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:31.107Z", + "updated_at": "2016-03-22T15:20:31.107Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 1249, + "note": "Nemo aut vitae et ducimus autem ex dolores.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:31.073Z", + "updated_at": "2016-03-22T15:20:31.073Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1248, + "note": "Repellendus eaque ex molestiae laudantium placeat quidem vitae recusandae.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:31.038Z", + "updated_at": "2016-03-22T15:20:31.038Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 12, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 12, + "state": "collected", + "st_commits": [ + { + "id": "97a0df9696e2aebf10c31b3016f40214e0e8f243", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T14:08:21.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T14:08:21.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 12, + "created_at": "2016-03-22T15:13:44.840Z", + "updated_at": "2016-03-22T15:13:44.908Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 11, + "target_branch": "test-3", + "source_branch": "test-5", + "source_project_id": 5, + "author_id": 26, + "assignee_id": 12, + "title": "Magni aut reprehenderit ut accusantium est eum.", + "created_at": "2016-03-22T15:13:44.494Z", + "updated_at": "2016-03-22T15:20:31.886Z", + "milestone_id": 10, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 3, + "description": "Et hic maxime harum ullam. Nulla velit pariatur libero recusandae. Dolor est earum laboriosam harum quo.", + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 1263, + "note": "Beatae incidunt exercitationem voluptates recusandae fuga quia enim.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:31.883Z", + "updated_at": "2016-03-22T15:20:31.883Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 1262, + "note": "Illum sunt id consequuntur fugit et quo ullam eum.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:31.860Z", + "updated_at": "2016-03-22T15:20:31.860Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1261, + "note": "Alias reiciendis autem ipsa sequi autem nemo odio.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:31.456Z", + "updated_at": "2016-03-22T15:20:31.456Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1260, + "note": "Maxime nisi odit eos nulla vel ex accusamus velit.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:31.426Z", + "updated_at": "2016-03-22T15:20:31.426Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1259, + "note": "Excepturi et qui sapiente ut ducimus sunt nesciunt.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:31.397Z", + "updated_at": "2016-03-22T15:20:31.397Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1258, + "note": "Quis rerum dolores et dolorem modi neque ullam doloribus.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:31.364Z", + "updated_at": "2016-03-22T15:20:31.364Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 1257, + "note": "Voluptatum et mollitia neque aut.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:31.328Z", + "updated_at": "2016-03-22T15:20:31.328Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1256, + "note": "Rerum laudantium dolor natus doloribus voluptas aliquid a.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:31.298Z", + "updated_at": "2016-03-22T15:20:31.298Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 11, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 4" + } + } + ], + "merge_request_diff": { + "id": 11, + "state": "collected", + "st_commits": [ + { + "id": "f998ac87ac9244f15e9c15109a6f4e62a54b779d", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T14:43:23.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T14:43:23.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 11, + "created_at": "2016-03-22T15:13:44.497Z", + "updated_at": "2016-03-22T15:13:44.547Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } + }, + { + "id": 10, + "target_branch": "test-6", + "source_branch": "test-7", + "source_project_id": 5, + "author_id": 22, + "assignee_id": 4, + "title": "Rerum commodi corporis quis qui fugit sed ut.", + "created_at": "2016-03-22T15:13:44.103Z", + "updated_at": "2016-03-22T15:20:32.096Z", + "milestone_id": 11, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 5, + "iid": 2, + "description": "Laudantium vel dignissimos aspernatur quis aut. Dolores et doloremque ipsa quia voluptate modi labore. Ipsa provident repellat error et nihil.", + "position": 0, + "locked_at": null, + "updated_by_id": null, + "merge_error": null, + "merge_params": { + + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "notes": [ + { + "id": 1271, + "note": "Quod ut ut quisquam et ut dolorem dolor.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:32.093Z", + "updated_at": "2016-03-22T15:20:32.093Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" + } + }, + { + "id": 1270, + "note": "Sed deserunt et explicabo rem repellat voluptatem.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:32.070Z", + "updated_at": "2016-03-22T15:20:32.070Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1269, + "note": "Veritatis architecto omnis consequatur et optio.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:32.046Z", + "updated_at": "2016-03-22T15:20:32.046Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1268, + "note": "Omnis suscipit odio molestiae debitis quia autem magni.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:32.019Z", + "updated_at": "2016-03-22T15:20:32.019Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1267, + "note": "Molestias est sunt est tempora consequatur cupiditate magnam.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:31.993Z", + "updated_at": "2016-03-22T15:20:31.993Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" + } + }, + { + "id": 1266, + "note": "Ratione blanditiis eveniet voluptatem nostrum rerum excepturi in molestiae.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:31.969Z", + "updated_at": "2016-03-22T15:20:31.969Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, + { + "id": 1265, + "note": "Illo voluptatibus vel odio ea.", + "noteable_type": "MergeRequest", + "author_id": 24, + "created_at": "2016-03-22T15:20:31.944Z", + "updated_at": "2016-03-22T15:20:31.944Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 10, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1264, + "note": "Earum veritatis quis facere itaque iure.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:31.919Z", + "updated_at": "2016-03-22T15:20:31.919Z", + "project_id": 5, "attachment": { "url": null }, "line_code": null, "commit_id": null, - "noteable_id": 1, + "noteable_id": 10, "system": false, "st_diff": null, "updated_by_id": null, - "is_award": false + "is_award": false, + "author": { + "name": "User 4" + } } - ] - } - ], - "labels": [ - { - "id": 1, - "title": "label1", - "color": "#990000", - "project_id": 6, - "created_at": "2016-04-13T14:40:34.704Z", - "updated_at": "2016-04-13T14:40:36.891Z", - "template": false, - "description": null - } - ], - "milestones": [ - { - "id": 1, - "title": "Milestone v1.2", - "project_id": 6, - "description": null, - "due_date": null, - "created_at": "2016-04-13T14:40:37.901Z", - "updated_at": "2016-04-13T14:40:37.901Z", - "state": "active", - "iid": 1 - } - ], - "snippets": [ - { - "id": 1, - "title": "Illo ipsa maxime magni aut.", - "content": "Excepturi delectus ut harum est molestiae dolor.", - "author_id": 10, - "project_id": 6, - "created_at": "2016-04-13T14:40:35.603Z", - "updated_at": "2016-04-13T14:40:36.903Z", - "file_name": "daphne.mraz", - "visibility_level": 0 - } - ], - "releases": [ - { - "id": 1, - "tag": "v1.1.0", - "description": "Awesome release", - "project_id": 6, - "created_at": "2016-04-13T14:40:36.223Z", - "updated_at": "2016-04-13T14:40:36.913Z" - } - ], - "events": [ - { - "id": 1, - "target_type": null, - "target_id": null, - "title": null, - "data": null, - "project_id": 6, - "created_at": "2016-04-13T14:40:40.122Z", - "updated_at": "2016-04-13T14:40:40.122Z", - "action": 8, - "author_id": 1 - } - ], - "project_members": [ - { - "id": 1, - "user": { - "id": 1, - "email": "norval.gulgowski@schambergerboyle.co.uk", - "created_at": "2016-04-13T14:40:30.963Z", - "updated_at": "2016-04-13T14:40:30.963Z", - "name": "Jalon Cormier DVM", - "admin": false, - "projects_limit": 42, - "skype": "", - "linkedin": "", - "twitter": "", - "authentication_token": "tt-mPSZFvRBu8QzkW1Ss", - "theme_id": 2, - "bio": null, - "username": "vance.turner1", - "can_create_group": true, - "can_create_team": false, - "state": "active", - "color_scheme_id": 1, - "notification_level": 1, - "password_expires_at": null, - "created_by_id": null, - "last_credential_check_at": null, - "avatar": { - "url": null - }, - "hide_no_ssh_key": false, - "website_url": "", - "notification_email": "norval.gulgowski@schambergerboyle.co.uk", - "hide_no_password": false, - "password_automatically_set": false, - "location": null, - "encrypted_otp_secret": null, - "encrypted_otp_secret_iv": null, - "encrypted_otp_secret_salt": null, - "otp_required_for_login": false, - "otp_backup_codes": null, - "public_email": "", - "dashboard": "projects", - "project_view": "readme", - "consumed_timestep": null, - "layout": "fixed", - "hide_project_limit": false, - "otp_grace_period_started_at": null, - "ldap_email": false, - "external": false + ], + "merge_request_diff": { + "id": 10, + "state": "collected", + "st_commits": [ + { + "id": "b42bb86cea49bdcef943e521584b7f417d8ddd3d", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:03:09.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:03:09.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 10, + "created_at": "2016-03-22T15:13:44.107Z", + "updated_at": "2016-03-22T15:13:44.190Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" } - } - ], - "merge_requests": [ + }, { - "id": 1, - "target_branch": "feature", - "source_branch": "master", - "source_project_id": 2, - "author_id": 5, - "assignee_id": null, - "title": "Dignissimos officia sit aut id dolor iure voluptatem expedita.", - "created_at": "2016-04-13T14:40:33.381Z", - "updated_at": "2016-04-13T14:40:39.850Z", - "milestone_id": null, + "id": 9, + "target_branch": "test-8", + "source_branch": "test-9", + "source_project_id": 5, + "author_id": 24, + "assignee_id": 3, + "title": "Saepe et neque ut vero nobis et voluptatum facere qui minima.", + "created_at": "2016-03-22T15:13:43.792Z", + "updated_at": "2016-03-22T15:20:32.309Z", + "milestone_id": 10, "state": "opened", - "merge_status": "can_be_merged", - "target_project_id": 6, + "merge_status": "unchecked", + "target_project_id": 5, "iid": 1, - "description": null, + "description": "Autem enim aliquam labore qui voluptas ut voluptatem. Et corrupti sit fuga dolores alias iusto voluptatem. Excepturi ut saepe accusamus neque distinctio.", "position": 0, "locked_at": null, "updated_by_id": null, "merge_error": null, "merge_params": { + }, "merge_when_build_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, - "merge_request_diff": { - "id": 1, - "state": "collected", - "st_commits": [ - { - "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" - ], - "authored_date": "2014-02-27T10:01:38.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T10:01:38.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - ], - "authored_date": "2014-02-27T09:57:31.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:57:31.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "d14d6c0abdd253381df51a723d58691b2ee1ab08" - ], - "authored_date": "2014-02-27T09:54:21.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:54:21.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" - }, - { - "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "c1acaa58bbcbc3eafe538cb8274ba387047b69f8" - ], - "authored_date": "2014-02-27T09:49:50.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:49:50.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" + "notes": [ + { + "id": 1279, + "note": "A corrupti nesciunt pariatur ea.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2016-03-22T15:20:32.307Z", + "updated_at": "2016-03-22T15:20:32.307Z", + "project_id": 5, + "attachment": { + "url": null }, - { - "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \n", - "parent_ids": [ - "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" - ], - "authored_date": "2014-02-27T09:48:32.000+01:00", - "author_name": "Dmitriy Zaporozhets", - "author_email": "dmitriy.zaporozhets@gmail.com", - "committed_date": "2014-02-27T09:48:32.000+01:00", - "committer_name": "Dmitriy Zaporozhets", - "committer_email": "dmitriy.zaporozhets@gmail.com" + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Administrator" } - ], - "st_diffs": [ - { - "diff": "Binary files a/.DS_Store and /dev/null differ\n", - "new_path": ".DS_Store", - "old_path": ".DS_Store", - "a_mode": "100644", - "b_mode": "0", - "new_file": false, - "renamed_file": false, - "deleted_file": true, - "too_large": false - }, - { - "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", - "new_path": ".gitignore", - "old_path": ".gitignore", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", - "new_path": ".gitmodules", - "old_path": ".gitmodules", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false - }, - { - "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", - "new_path": "files/.DS_Store", - "old_path": "files/.DS_Store", - "a_mode": "100644", - "b_mode": "0", - "new_file": false, - "renamed_file": false, - "deleted_file": true, - "too_large": false + }, + { + "id": 1278, + "note": "Adipisci aut ut et voluptate numquam.", + "noteable_type": "MergeRequest", + "author_id": 3, + "created_at": "2016-03-22T15:20:32.281Z", + "updated_at": "2016-03-22T15:20:32.281Z", + "project_id": 5, + "attachment": { + "url": null }, - { - "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", - "new_path": "files/ruby/popen.rb", - "old_path": "files/ruby/popen.rb", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Alexie Trantow" + } + }, + { + "id": 1277, + "note": "Adipisci voluptatem quod ut placeat repellendus deleniti.", + "noteable_type": "MergeRequest", + "author_id": 4, + "created_at": "2016-03-22T15:20:32.255Z", + "updated_at": "2016-03-22T15:20:32.255Z", + "project_id": 5, + "attachment": { + "url": null }, - { - "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", - "new_path": "files/ruby/regex.rb", - "old_path": "files/ruby/regex.rb", - "a_mode": "100644", - "b_mode": "100644", - "new_file": false, - "renamed_file": false, - "deleted_file": false, - "too_large": false + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Julius Moore" + } + }, + { + "id": 1276, + "note": "Vitae et doloremque aut et aspernatur velit placeat sed.", + "noteable_type": "MergeRequest", + "author_id": 10, + "created_at": "2016-03-22T15:20:32.230Z", + "updated_at": "2016-03-22T15:20:32.230Z", + "project_id": 5, + "attachment": { + "url": null }, - { - "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", - "new_path": "gitlab-grack", - "old_path": "gitlab-grack", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false, - "too_large": false + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Robyn McCullough Jr." + } + }, + { + "id": 1275, + "note": "Quos cupiditate nesciunt expedita aspernatur.", + "noteable_type": "MergeRequest", + "author_id": 12, + "created_at": "2016-03-22T15:20:32.207Z", + "updated_at": "2016-03-22T15:20:32.207Z", + "project_id": 5, + "attachment": { + "url": null }, - { - "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", - "new_path": "gitlab-shell", - "old_path": "gitlab-shell", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false, - "too_large": false + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "Vladimir McCullough" } - ], - "merge_request_id": 1, - "created_at": "2016-04-13T14:40:33.474Z", - "updated_at": "2016-04-13T14:40:33.834Z", - "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", - "real_size": "8" - }, - "notes": [ + }, + { + "id": 1274, + "note": "Optio rem inventore dicta praesentium sit.", + "noteable_type": "MergeRequest", + "author_id": 22, + "created_at": "2016-03-22T15:20:32.181Z", + "updated_at": "2016-03-22T15:20:32.181Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 0" + } + }, { - "id": 2, - "note": ":+1: merge_request", + "id": 1273, + "note": "Sit incidunt molestiae maxime officiis rerum necessitatibus.", "noteable_type": "MergeRequest", "author_id": 24, - "created_at": "2016-04-13T14:40:39.832Z", - "updated_at": "2016-04-13T14:40:39.832Z", - "project_id": 9, + "created_at": "2016-03-22T15:20:32.159Z", + "updated_at": "2016-03-22T15:20:32.159Z", + "project_id": 5, + "attachment": { + "url": null + }, + "line_code": null, + "commit_id": null, + "noteable_id": 9, + "system": false, + "st_diff": null, + "updated_by_id": null, + "is_award": false, + "author": { + "name": "User 2" + } + }, + { + "id": 1272, + "note": "Autem ut non itaque molestiae nisi quia officiis doloribus.", + "noteable_type": "MergeRequest", + "author_id": 26, + "created_at": "2016-03-22T15:20:32.129Z", + "updated_at": "2016-03-22T15:20:32.129Z", + "project_id": 5, "attachment": { "url": null }, "line_code": null, "commit_id": null, - "noteable_id": 1, + "noteable_id": 9, "system": false, "st_diff": null, "updated_by_id": null, - "is_award": false + "is_award": false, + "author": { + "name": "User 4" + } } - ] + ], + "merge_request_diff": { + "id": 9, + "state": "collected", + "st_commits": [ + { + "id": "e239ba8c97b80b2874579a4d625ea9628f4c8ff5", + "message": "fixes #10\n", + "parent_ids": [ + "be93687618e4b132087f430a4d8fc3a609c9b77c" + ], + "authored_date": "2016-01-19T15:38:06.000+01:00", + "author_name": "Test Lopez", + "author_email": "Test@Testlopez.es", + "committed_date": "2016-01-19T15:38:06.000+01:00", + "committer_name": "Test Lopez", + "committer_email": "Test@Testlopez.es" + } + ], + "st_diffs": [ + { + "diff": "--- /dev/null\n+++ b/test\n", + "new_path": "test", + "old_path": "test", + "a_mode": "0", + "b_mode": "100644", + "new_file": true, + "renamed_file": false, + "deleted_file": false, + "too_large": false + } + ], + "merge_request_id": 9, + "created_at": "2016-03-22T15:13:43.794Z", + "updated_at": "2016-03-22T15:13:43.848Z", + "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "real_size": "1" + } } ], "ci_commits": [ { - "id": 2, - "project_id": 6, + "id": 36, + "project_id": 5, + "ref": "master", + "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.755Z", + "updated_at": "2016-03-22T15:20:35.755Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 71, + "project_id": 5, + "status": "failed", + "finished_at": "2016-03-29T06:28:12.630Z", + "trace": null, + "created_at": "2016-03-22T15:20:35.772Z", + "updated_at": "2016-03-29T06:28:12.634Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 36, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 72, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.", + "created_at": "2016-03-22T15:20:35.777Z", + "updated_at": "2016-03-22T15:20:35.777Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 36, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 37, + "project_id": 5, "ref": "master", - "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + "sha": "048721d90c449b244b7b4c53a9186b04330174ec", "before_sha": null, "push_data": null, - "created_at": "2016-04-13T14:40:37.759Z", - "updated_at": "2016-04-13T14:40:37.759Z", - "tag": false, + "created_at": "2016-03-22T15:20:35.757Z", + "updated_at": "2016-03-22T15:20:35.757Z", + "tag": null, "yaml_errors": null, "committed_at": null, - "gl_project_id": 6, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, "statuses": [ { - "id": 1, - "project_id": null, + "id": 74, + "project_id": 5, "status": "success", - "finished_at": "2016-01-26T07:23:42.000Z", + "finished_at": null, + "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.", + "created_at": "2016-03-22T15:20:35.846Z", + "updated_at": "2016-03-22T15:20:35.846Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 37, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 73, + "project_id": 5, + "status": "canceled", + "finished_at": null, "trace": null, - "created_at": "2016-04-13T14:40:37.717Z", - "updated_at": "2016-04-13T14:40:37.771Z", - "started_at": "2016-01-26T07:21:42.000Z", + "created_at": "2016-03-22T15:20:35.842Z", + "updated_at": "2016-03-22T15:20:35.842Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 37, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 38, + "project_id": 5, + "ref": "master", + "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.759Z", + "updated_at": "2016-03-22T15:20:35.759Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 76, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.", + "created_at": "2016-03-22T15:20:35.882Z", + "updated_at": "2016-03-22T15:20:35.882Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 38, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 75, + "project_id": 5, + "status": "failed", + "finished_at": null, + "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.", + "created_at": "2016-03-22T15:20:35.864Z", + "updated_at": "2016-03-22T15:20:35.864Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 38, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 39, + "project_id": 5, + "ref": "master", + "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.761Z", + "updated_at": "2016-03-22T15:20:35.761Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 78, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.", + "created_at": "2016-03-22T15:20:35.927Z", + "updated_at": "2016-03-22T15:20:35.927Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 39, + "commands": "$ build command", + "job_id": null, + "name": "test build 2", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 77, + "project_id": 5, + "status": "failed", + "finished_at": null, + "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.", + "created_at": "2016-03-22T15:20:35.905Z", + "updated_at": "2016-03-22T15:20:35.905Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 39, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" + }, + "erased_by_id": null, + "erased_at": null + } + ] + }, + { + "id": 40, + "project_id": 5, + "ref": "master", + "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.763Z", + "updated_at": "2016-03-22T15:20:35.763Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "gl_project_id": 5, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "statuses": [ + { + "id": 79, + "project_id": 5, + "status": "failed", + "finished_at": "2016-03-29T06:28:12.695Z", + "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.", + "created_at": "2016-03-22T15:20:35.950Z", + "updated_at": "2016-03-29T06:28:12.696Z", + "started_at": null, + "runner_id": null, + "coverage": null, + "commit_id": 40, + "commands": "$ build command", + "job_id": null, + "name": "test build 1", + "deploy": false, + "options": null, + "allow_failure": false, + "stage": "test", + "trigger_request_id": null, + "stage_idx": 1, + "tag": null, + "ref": "master", + "user_id": null, + "target_url": null, + "description": null, + "artifacts_file": { + "url": null + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": null + }, + "erased_by_id": null, + "erased_at": null + }, + { + "id": 80, + "project_id": 5, + "status": "success", + "finished_at": null, + "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.", + "created_at": "2016-03-22T15:20:35.966Z", + "updated_at": "2016-03-22T15:20:35.966Z", + "started_at": null, "runner_id": null, "coverage": null, - "commit_id": 2, - "commands": null, + "commit_id": 40, + "commands": "$ build command", "job_id": null, - "name": "default", + "name": "test build 2", "deploy": false, "options": null, "allow_failure": false, - "stage": null, + "stage": "test", "trigger_request_id": null, - "stage_idx": null, + "stage_idx": 1, "tag": null, - "ref": null, + "ref": "master", "user_id": null, "target_url": null, - "description": "commit status", - "artifacts_file": null, - "gl_project_id": 7, - "artifacts_metadata": null, + "description": null, + "artifacts_file": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" + }, + "gl_project_id": 5, + "artifacts_metadata": { + "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" + }, "erased_by_id": null, "erased_at": null } -- cgit v1.2.1 From c487e6d311957984da95d19ed8d63b068c6da228 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 16:24:32 +0200 Subject: updated spec file --- .../import_export/test_project_export.tar.gz | Bin 338822 -> 339384 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index f2c3c3cd2dc..aef4af6998e 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 1eb802cde331fa8b3e18b45d0d3f81061661a22f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 16:25:27 +0200 Subject: fixed leaving comments on notes about missing authors --- lib/gitlab/import_export/import_service.rb | 7 ++++++- lib/gitlab/import_export/members_mapper.rb | 13 +++++++------ lib/gitlab/import_export/project_tree_restorer.rb | 2 +- lib/gitlab/import_export/relation_factory.rb | 9 +++++---- spec/lib/gitlab/import_export/members_mapper_spec.rb | 2 +- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 670f1ebece8..0c483884fe9 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -16,7 +16,12 @@ module Gitlab def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, shared: @shared) - project_tree.project if [restore_project_tree, restore_repo, restore_wiki_repo].all? + if [restore_project_tree, restore_repo, restore_wiki_repo].all? + project_tree.project + else + project_tree.project.destroy if project_tree.project + nil + end end private diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index da8aa475653..5332529a90f 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -4,10 +4,10 @@ module Gitlab attr_reader :map, :note_member_list - def initialize(exported_members:, user:, project_id:) + def initialize(exported_members:, user:, project:) @exported_members = exported_members @user = user - @project_id = project_id + @project = project @note_member_list = [] @project_member_map = Hash.new do |_, key| @@ -36,20 +36,21 @@ module Gitlab end def member_hash(member) - member.except('id').merge(source_id: @project_id) + member.except('id').merge(source_id: @project.id) end - #TODO: If default, then we need to leave a comment 'Comment by ' on comments def default_project_member @default_project_member ||= begin + return @project.project_members.first.user.id unless @project.project_members.empty? default_member = ProjectMember.new(default_project_member_hash) - default_member.user.id if default_member.save + default_member.save! + default_member.user.id end end def default_project_member_hash - { user: @user, access_level: ProjectMember::MASTER, source_id: @project_id } + { user: @user, access_level: ProjectMember::MASTER, source_id: @project.id } end def find_project_user_query(member) diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index a840c9f9478..bd343d0b695 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -29,7 +29,7 @@ module Gitlab def members_mapper @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members, user: @user, - project_id: project.id) + project: project) end def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 0adcd0d5e6c..cdd4987f980 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -30,16 +30,17 @@ module Gitlab def update_missing_author(relation_hash, members_map) old_author_id = relation_hash['author_id'] relation_hash['author_id'] = members_map.map[old_author_id] + author = relation_hash.delete('author') + return unless members_map.note_member_list.include?(old_author_id) relation_hash['note'] = ('*Blank note*') if relation_hash['note'].blank? - relation_hash['note'] += (missing_author_note(relation_hash['updated_at'], - relation_hash['author']['name'])) - relation_hash.delete('author') + relation_hash['note'] += (missing_author_note(relation_hash['updated_at'], author['name'])) end def missing_author_note(updated_at, author_name) - "\n\n *By #{author_name} on #{updated_at} (imported from GitLab project)*" + timestamp = updated_at.split('.').first + "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" end def update_project_references(relation_hash, klass) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 78706f64fb7..adaadbff6f8 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do let(:members_mapper) do described_class.new( - exported_members: exported_members, user: user, project_id: project.id) + exported_members: exported_members, user: user, project: project) end it 'maps a project member' do -- cgit v1.2.1 From 8c508037a63ba2b82d56757bce011ac53a073ae6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 17:03:03 +0200 Subject: updated controllers with permissions check --- .../import/gitlab_project_controller.rb | 45 ---------------------- .../import/gitlab_projects_controller.rb | 41 +++++++++----------- app/controllers/projects_controller.rb | 2 +- 3 files changed, 19 insertions(+), 69 deletions(-) delete mode 100644 app/controllers/import/gitlab_project_controller.rb diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb deleted file mode 100644 index ab0da196ac1..00000000000 --- a/app/controllers/import/gitlab_project_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Import::GitlabProjectController < Import::BaseController - before_action :verify_gitlab_project_import_enabled - before_action :gitlab_project_auth, except: :callback - - rescue_from OAuth::Error, with: :gitlab_project_unauthorized - - #TODO permissions stuff - - def callback - - redirect_to status_import_gitlab_project_url - end - - def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects - - @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } - end - - def jobs - jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) - render json: jobs - end - - def create - @file = params[:file] - - repo_owner = current_user.username - @target_namespace = params[:new_namespace].presence || repo_owner - - # namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) - end - - private - - def verify_gitlab_project_import_enabled - render_404 unless gitlab_project_import_enabled? - end -end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index f8d4bcff55a..41b72c33ffe 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -1,43 +1,38 @@ class Import::GitlabProjectsController < Import::BaseController before_action :verify_gitlab_project_import_enabled - #before_action :gitlab_project_auth, except: :callback + before_action :verify_project_and_namespace_access rescue_from OAuth::Error, with: :gitlab_project_unauthorized - #TODO permissions stuff - def new @namespace_id = project_params[:namespace_id] @path = project_params[:path] end - def status - - end + def create + @project = Project.create_from_import_job(current_user_id: current_user.id, + tmp_file: File.expand_path(params[:file].path), + namespace_id: project_params[:namespace_id], + project_path: project_params[:path]) - def jobs - jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) - render json: jobs + redirect_to dashboard_projects_path end - def create - # TODO verify access to namespace and path - file = params[:file] - namespace_id = project_params[:namespace_id] - path = project_params[:path] - - repo_owner = current_user.username - @target_namespace = params[:new_namespace].presence || repo_owner + private - @project = Project.create_from_import_job(current_user_id: current_user.id, - tmp_file: File.expand_path(file.path), - namespace_id: namespace_id, - project_path: path) + def verify_project_and_namespace_access + unless namespace_access? && project_access? + render_403 + end + end - redirect_to status_import_gitlab_project_path + def project_access? + can?(current_user, :admin_project, @project) end - private + def namespace_access? + current_user.can?(:create_projects, Namespace.find(project_params[:namespace_id])) + end def verify_gitlab_project_import_enabled render_404 unless gitlab_project_import_enabled? diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index f15f20dcf52..980cae65f84 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? # Authorize - before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] + before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export] before_action :event_filter, only: [:show, :activity] layout :determine_layout -- cgit v1.2.1 From 89fb5df534c5b7c1ffc147e9274aee9db4f4cbc4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 17:08:19 +0200 Subject: updated routes file --- config/routes.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 871997d01a2..08598e36fe3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -160,9 +160,7 @@ Rails.application.routes.draw do end resource :gitlab_project, only: [:create, :new], controller: :gitlab_projects do - get :status post :create - get :jobs end end -- cgit v1.2.1 From 1ee19f04fd51322606d641ba82e6434d8d5c186f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 17:16:24 +0200 Subject: fixed permissions --- app/controllers/import/gitlab_projects_controller.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 41b72c33ffe..a4ac55cd2a4 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -21,15 +21,11 @@ class Import::GitlabProjectsController < Import::BaseController private def verify_project_and_namespace_access - unless namespace_access? && project_access? + unless namespace_access? render_403 end end - def project_access? - can?(current_user, :admin_project, @project) - end - def namespace_access? current_user.can?(:create_projects, Namespace.find(project_params[:namespace_id])) end -- cgit v1.2.1 From 2e1decd061d679b3b8acd0a3e2178b61b59af65d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 17:17:46 +0200 Subject: restricted actual member mapping to admins --- lib/gitlab/import_export/members_mapper.rb | 20 ++++++++++---------- lib/gitlab/import_export/project_tree_restorer.rb | 6 ++++-- lib/gitlab/import_export/relation_factory.rb | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 5332529a90f..a5fdac6d93b 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -18,6 +18,16 @@ module Gitlab @map = generate_map end + def default_project_member + @default_project_member ||= + begin + return @project.project_members.first.user.id unless @project.project_members.empty? + default_member = ProjectMember.new(default_project_member_hash) + default_member.save! + default_member.user.id + end + end + private def generate_map @@ -39,16 +49,6 @@ module Gitlab member.except('id').merge(source_id: @project.id) end - def default_project_member - @default_project_member ||= - begin - return @project.project_members.first.user.id unless @project.project_members.empty? - default_member = ProjectMember.new(default_project_member_hash) - default_member.save! - default_member.user.id - end - end - def default_project_member_hash { user: @user, access_level: ProjectMember::MASTER, source_id: @project.id } end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index bd343d0b695..911ba06e748 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -94,8 +94,10 @@ module Gitlab end def relation_from_factory(relation, relation_hash) - Gitlab::ImportExport::RelationFactory.create( - relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_mapper: members_mapper) + Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, + relation_hash: relation_hash.merge('project_id' => project.id), + members_mapper: members_mapper, + user_admin: @user.is_admin?) end end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index cdd4987f980..3b27f133ecf 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -6,11 +6,11 @@ module Gitlab OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze - def create(relation_sym:, relation_hash:, members_mapper:) + def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) relation_sym = parse_relation_sym(relation_sym) klass = parse_relation(relation_hash, relation_sym) - update_missing_author(relation_hash, members_mapper) if relation_sym == :notes + update_missing_author(relation_hash, members_mapper, user_admin) if relation_sym == :notes update_user_references(relation_hash, members_mapper.map) update_project_references(relation_hash, klass) @@ -27,12 +27,19 @@ module Gitlab end end - def update_missing_author(relation_hash, members_map) + def update_missing_author(relation_hash, members_map, user_admin) old_author_id = relation_hash['author_id'] - relation_hash['author_id'] = members_map.map[old_author_id] + + # Users with admin access have access to mapping of users + if user_admin + relation_hash['author_id'] = members_map.default_project_member + else + relation_hash['author_id'] = members_map.map[old_author_id] + end + author = relation_hash.delete('author') - return unless members_map.note_member_list.include?(old_author_id) + return unless user_admin && members_map.note_member_list.include?(old_author_id) relation_hash['note'] = ('*Blank note*') if relation_hash['note'].blank? relation_hash['note'] += (missing_author_note(relation_hash['updated_at'], author['name'])) -- cgit v1.2.1 From 2dff04f24a8446216e12d43fab6841f28f3dd406 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 17:39:03 +0200 Subject: fixed TODOs left --- lib/gitlab/import_export.rb | 8 ++++++++ lib/gitlab/import_export/project_tree_saver.rb | 7 +------ lib/gitlab/import_export/repo_bundler.rb | 7 +------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 6835411ba70..bf80ac7f093 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -9,5 +9,13 @@ module Gitlab def storage_path File.join(Settings.shared['path'], 'tmp/project_exports') end + + def project_filename + "project.json" + end + + def project_bundle_filename + "project.bundle" + end end end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 2287524c8a5..c894295b0f5 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -6,7 +6,7 @@ module Gitlab def initialize(project:, shared:) @project = project @shared = shared - @full_path = File.join(@shared.export_path, project_filename) + @full_path = File.join(@shared.export_path, ImportExport.project_filename) end def save @@ -20,11 +20,6 @@ module Gitlab private - # TODO remove magic keyword and move it to a shared config - def project_filename - "project.json" - end - def project_json_tree @project.to_json(Gitlab::ImportExport::ImportExportReader.new(shared: @shared).project_tree) end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index bcf976fb624..aff6e92be70 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -12,7 +12,7 @@ module Gitlab def bundle return false if @project.empty_repo? - @full_path = File.join(@shared.export_path, project_filename) + @full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename) bundle_to_disk end @@ -26,11 +26,6 @@ module Gitlab false end - # TODO remove magic keyword and move it to a shared config - def project_filename - "project.bundle" - end - def path_to_repo @project.repository.path_to_repo end -- cgit v1.2.1 From bf81c588fd7f34e475c0f317f7e49dc70e36366d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 19:51:07 +0200 Subject: fix issue with mapping members --- lib/gitlab/import_export/command_line_util.rb | 6 ------ lib/gitlab/import_export/members_mapper.rb | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index f53f8fba96f..c99b0d83b41 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -19,12 +19,6 @@ module Gitlab private - def git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) - cmd = %W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}) - _output, status = Gitlab::Popen.popen(cmd) - status.zero? - end - def tar_with_options(archive:, dir:, options:) execute(%W(tar -#{options} #{archive} -C #{dir} .)) end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index a5fdac6d93b..4401fab3d23 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -10,6 +10,10 @@ module Gitlab @project = project @note_member_list = [] + # This needs to run first, as second call would be from generate_map + # which means project members already exist. + default_project_member + @project_member_map = Hash.new do |_, key| @note_member_list << key default_project_member -- cgit v1.2.1 From 220e55a9806c4de9fc6839b1c676ba022b83f720 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 May 2016 20:58:27 +0200 Subject: fix spec --- spec/lib/gitlab/import_export/project_tree_restorer_spec.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 6464b0141f3..bc7993b9d75 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -5,9 +5,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "../../../spec/lib/gitlab/import_export/", project_path: 'path') } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, namespace_id: namespace.id) } - let!(:restored_project_json) { project_tree_restorer.restore } + let(:restored_project_json) { project_tree_restorer.restore } + + before do + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + end context 'JSON' do it 'restores models based on JSON' do -- cgit v1.2.1 From f386d7a7b7f67ba8e77ee40ef6a3383d5206d0c1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 10:13:00 +0200 Subject: some changes based on MR feedback --- lib/gitlab/import_export/command_line_util.rb | 8 ++++++-- lib/gitlab/import_export/import_export_reader.rb | 1 + lib/gitlab/import_export/repo_bundler.rb | 2 +- lib/gitlab/import_export/wiki_repo_bundler.rb | 11 +++++------ spec/lib/gitlab/import_export/import_export_reader_spec.rb | 6 +++--- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index c99b0d83b41..78664f076eb 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -9,11 +9,11 @@ module Gitlab untar_with_options(archive: archive, dir: dir, options: 'zxf') end - def git_bundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + def git_bundle(repo_path:, bundle_path:) execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) end - def git_unbundle(git_bin_path: Gitlab.config.git.bin_path, repo_path:, bundle_path:) + def git_unbundle(repo_path:, bundle_path:) execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) end @@ -31,6 +31,10 @@ module Gitlab _output, status = Gitlab::Popen.popen(cmd) status.zero? end + + def git_bin_path + Gitlab.config.git.bin_path + end end end end diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 9aac0eae79a..25e13074e90 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -14,6 +14,7 @@ module Gitlab @attributes_parser.find_included(:project).merge(include: build_hash(@tree)) rescue => e @shared.error(e.message) + false end private diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_bundler.rb index aff6e92be70..f41d5af4e53 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_bundler.rb @@ -5,7 +5,7 @@ module Gitlab attr_reader :full_path - def initialize(project: , shared: ) + def initialize(project:, shared:) @project = project @shared = shared end diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_bundler.rb index a0000176bb5..016c640ae15 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_bundler.rb @@ -3,14 +3,13 @@ module Gitlab class WikiRepoBundler < RepoBundler def bundle @wiki = ProjectWiki.new(@project) - return true if !wiki? # it's okay to have no Wiki - @full_path = File.join(@shared.export_path, project_filename) - bundle_to_disk + return true unless wiki_repository_exists? # it's okay to have no Wiki + bundle_to_disk(File.join(@shared.export_path, project_filename)) end - def bundle_to_disk + def bundle_to_disk(full_path) FileUtils.mkdir_p(@shared.export_path) - git_bundle(repo_path: path_to_repo, bundle_path: @full_path) + git_bundle(repo_path: path_to_repo, bundle_path: full_path) rescue => e @shared.error(e.message) false @@ -26,7 +25,7 @@ module Gitlab @wiki.repository.path_to_repo end - def wiki? + def wiki_repository_exists? File.exists?(@wiki.repository.path_to_repo) && !@wiki.repository.empty? end end diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb index 2fc9a39c68b..c4d03fecd54 100644 --- a/spec/lib/gitlab/import_export/import_export_reader_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ImportExport::ImportExportReader do +describe Gitlab::ImportExport::ImportExportReader, lib: true do let(:shared) { Gitlab::ImportExport::Shared.new(relative_path:'') } let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do @@ -16,7 +16,7 @@ describe Gitlab::ImportExport::ImportExportReader do } end - it 'should generate hash from project tree config' do - expect(described_class.new(config: test_config, shared: shared).project_tree) =~ (project_tree_hash) + it 'generates hash from project tree config' do + expect(described_class.new(config: test_config, shared: shared).project_tree).to match(project_tree_hash) end end -- cgit v1.2.1 From 30aa37c8127dfe10df44f7945a4722b95581ff24 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 11:24:20 +0200 Subject: remove spec change --- spec/spec_helper.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5b90a4b534b..576d16e7ea3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -52,7 +52,3 @@ FactoryGirl::SyntaxRunner.class_eval do end ActiveRecord::Migration.maintain_test_schema! - -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, timeout: 1.minute) -end -- cgit v1.2.1 From 5777ad9a1f016f170585949059b08a4eeb7d19a9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 12:04:31 +0200 Subject: adding versioning to export --- .../projects/import_export/export_service.rb | 11 +++++--- lib/gitlab/import_export.rb | 6 +++++ lib/gitlab/import_export/version_saver.rb | 29 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 lib/gitlab/import_export/version_saver.rb diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index e6eb6f915b0..0691ca9d468 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -4,12 +4,16 @@ module Projects def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) - save_all if [save_project_tree, bundle_repo, bundle_wiki_repo].all? - notify_worker if @shared.errors.any? + save_all if [save_version, save_project_tree, bundle_repo, bundle_wiki_repo].all? + cleanup_and_notify_worker if @shared.errors.any? end private + def save_version + Gitlab::ImportExport::VersionSaver.save(shared: @shared) + end + def save_project_tree Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save end @@ -26,7 +30,8 @@ module Projects Gitlab::ImportExport::Saver.save(shared: @shared) end - def notify_worker + def cleanup_and_notify_worker + FileUtils.rm_rf(@shared.export_path) raise Gitlab::ImportExport::Error.new(@shared.errors.join(', ')) end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index bf80ac7f093..9c7c72e2b4a 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -2,6 +2,8 @@ module Gitlab module ImportExport extend self + VERSION = '0.1.0' + def export_path(relative_path:) File.join(storage_path, relative_path) end @@ -17,5 +19,9 @@ module Gitlab def project_bundle_filename "project.bundle" end + + def version_filename + 'VERSION' + end end end diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb new file mode 100644 index 00000000000..e8be52b990f --- /dev/null +++ b/lib/gitlab/import_export/version_saver.rb @@ -0,0 +1,29 @@ +module Gitlab + module ImportExport + class VersionSaver + + def self.save(*args) + new(*args).save + end + + def initialize(shared:) + @shared = shared + end + + def save + File.open(version_file, 'w') do |file| + file.write(Gitlab::ImportExport.VERSION) + end + rescue => e + @shared.error(e.message) + false + end + + private + + def version_file + File.join(@shared.export_path, Gitlab::ImportExport.version_filename) + end + end + end +end -- cgit v1.2.1 From 51487575bca1f4cdce4919bb4cb2bbb1ee686368 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 12:39:42 +0200 Subject: added version check on import --- lib/gitlab/import_export/import_service.rb | 6 ++++- lib/gitlab/import_export/version_restorer.rb | 36 ++++++++++++++++++++++++++++ lib/gitlab/import_export/version_saver.rb | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 lib/gitlab/import_export/version_restorer.rb diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 0c483884fe9..b025bff29bd 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -16,7 +16,7 @@ module Gitlab def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, shared: @shared) - if [restore_project_tree, restore_repo, restore_wiki_repo].all? + if [restore_version, restore_project_tree, restore_repo, restore_wiki_repo].all? project_tree.project else project_tree.project.destroy if project_tree.project @@ -26,6 +26,10 @@ module Gitlab private + def restore_version + Gitlab::ImportExport::VersionRestorer.restore(shared: @shared) + end + def restore_project_tree project_tree.restore end diff --git a/lib/gitlab/import_export/version_restorer.rb b/lib/gitlab/import_export/version_restorer.rb new file mode 100644 index 00000000000..797696511de --- /dev/null +++ b/lib/gitlab/import_export/version_restorer.rb @@ -0,0 +1,36 @@ +module Gitlab + module ImportExport + class VersionRestorer + + def self.restore(*args) + new(*args).restore + end + + def initialize(shared:) + @shared = shared + end + + def restore + version = File.open(version_file, &:readline) + verify_version!(version) + rescue => e + @shared.error(e) + false + end + + private + + def version_file + File.join(@shared.export_path, Gitlab::ImportExport.version_filename) + end + + def verify_version!(version) + if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.VERSION) + raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.VERSION} but was #{version}") + else + true + end + end + end + end +end diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb index e8be52b990f..197271f3555 100644 --- a/lib/gitlab/import_export/version_saver.rb +++ b/lib/gitlab/import_export/version_saver.rb @@ -15,7 +15,7 @@ module Gitlab file.write(Gitlab::ImportExport.VERSION) end rescue => e - @shared.error(e.message) + @shared.error(e) false end -- cgit v1.2.1 From c374700390ad0e3ab8a7ad954ee1b6e311a48129 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 12:40:01 +0200 Subject: missing new line --- lib/gitlab/import_export/version_restorer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/import_export/version_restorer.rb b/lib/gitlab/import_export/version_restorer.rb index 797696511de..bab2fecec49 100644 --- a/lib/gitlab/import_export/version_restorer.rb +++ b/lib/gitlab/import_export/version_restorer.rb @@ -34,3 +34,4 @@ module Gitlab end end end + -- cgit v1.2.1 From 360689bb5834cfe5b57c121d71a5123f27081fb0 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 13:11:49 +0200 Subject: fix version --- lib/gitlab/import_export/version_restorer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/version_restorer.rb b/lib/gitlab/import_export/version_restorer.rb index bab2fecec49..2c83bf08997 100644 --- a/lib/gitlab/import_export/version_restorer.rb +++ b/lib/gitlab/import_export/version_restorer.rb @@ -25,8 +25,8 @@ module Gitlab end def verify_version!(version) - if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.VERSION) - raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.VERSION} but was #{version}") + if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version) + raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}") else true end -- cgit v1.2.1 From 504c186f7139df71dbea596b422d833f32348f46 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 13:28:11 +0200 Subject: fix version issue --- lib/gitlab/import_export.rb | 4 ++++ lib/gitlab/import_export/project_tree_saver.rb | 1 - lib/gitlab/import_export/version_saver.rb | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 9c7c72e2b4a..65a8fcfadd0 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -23,5 +23,9 @@ module Gitlab def version_filename 'VERSION' end + + def version + VERSION + end end end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index c894295b0f5..631746a47f1 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -10,7 +10,6 @@ module Gitlab end def save - FileUtils.mkdir_p(@shared.export_path) File.write(full_path, project_json_tree) true rescue => e diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb index e8be52b990f..904645f273e 100644 --- a/lib/gitlab/import_export/version_saver.rb +++ b/lib/gitlab/import_export/version_saver.rb @@ -11,8 +11,10 @@ module Gitlab end def save + FileUtils.mkdir_p(@shared.export_path) + File.open(version_file, 'w') do |file| - file.write(Gitlab::ImportExport.VERSION) + file.write(Gitlab::ImportExport.version) end rescue => e @shared.error(e.message) -- cgit v1.2.1 From 7ea68d006e93f4bfe59d0513696e46c1ad5ee6a1 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 15:17:44 +0200 Subject: updated file import spec --- .../import_export/test_project_export.tar.gz | Bin 339384 -> 339482 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index aef4af6998e..4f2ec49e9b5 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 279915253c9116c8658d1fafffd7efb74ee249d7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 16 May 2016 15:29:14 +0200 Subject: fix issue with forked MRs --- lib/gitlab/import_export/relation_factory.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 3b27f133ecf..082398d1f0f 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -53,11 +53,19 @@ module Gitlab def update_project_references(relation_hash, klass) project_id = relation_hash.delete('project_id') + if relation_hash['source_project_id'] && relation_hash['target_project_id'] + # If source and target are the same, populate them with the new project ID. + if relation_hash['target_project_id'] == relation_hash['source_project_id'] + relation_hash['source_project_id'] = project_id + else + relation_hash['source_project_id'] = -1 + end + end + relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + # project_id may not be part of the export, but we always need to populate it if required. relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] - relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] - relation_hash['source_project_id'] = -1 if relation_hash['source_project_id'] end def relation_class(relation_sym) -- cgit v1.2.1 From 07c642cb1342c82ff04681964754bc654450ec16 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 09:39:34 +0200 Subject: updated routes for gitlab_project --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 08598e36fe3..a528e0b4940 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,7 +159,7 @@ Rails.application.routes.draw do post :create_user_map, path: :user_map end - resource :gitlab_project, only: [:create, :new], controller: :gitlab_projects do + resource :gitlab_project, only: [:create, :new] do post :create end end -- cgit v1.2.1 From f48184cbc6d8951156ac5af6821f0a47dbc1c588 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 09:40:49 +0200 Subject: updated routes for gitlab_projects --- config/routes.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 8ad0b4874df..ad44c8f05b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,10 +159,8 @@ Rails.application.routes.draw do post :create_user_map, path: :user_map end - resource :gitlab_project, only: [:create, :new], controller: :gitlab_projects do - get :status - get :callback - get :jobs + resource :gitlab_project, only: [:create, :new] do + post :create end end -- cgit v1.2.1 From 7733f56d80f84ada4c4fef8cc4a5ab9357af0dc5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 16:28:27 +0200 Subject: fix specs --- lib/gitlab/import_export/project_tree_saver.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 631746a47f1..a2c5df8af25 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -10,6 +10,8 @@ module Gitlab end def save + FileUtils.mkdir_p(@shared.export_path) + File.write(full_path, project_json_tree) true rescue => e -- cgit v1.2.1 From bcda64c75c15e2fb1343378a9d07e4659ae4acda Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 10:39:53 +0200 Subject: added DB configuration --- lib/gitlab/import_export/import_export.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 66e6ae86cfe..ae2bf677c90 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -16,6 +16,13 @@ project_tree: - :merge_request_diff - ci_commits: - :statuses + - :builds + - :variables + - :triggers + - :deploy_keys + - :services + - :hooks + - :protected_branches # Only include the following attributes for the models specified. included_attributes: -- cgit v1.2.1 From ee2993040c207f70bf05a730b9389a54928e5a54 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 11:20:38 +0200 Subject: updated test file with DB config --- .../import_export/test_project_export.tar.gz | Bin 339482 -> 340727 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 4f2ec49e9b5..380696d5ab5 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 4d894a7a953f18d8a71f003677b5f8ea5bbc1779 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 14:32:30 +0200 Subject: added commits mapper and DB config import stuff --- lib/gitlab/import_export/commit_mapper.rb | 49 +++++++++++++++++ lib/gitlab/import_export/project_tree_restorer.rb | 14 +++-- lib/gitlab/import_export/relation_factory.rb | 66 ++++++++++++++++------- 3 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 lib/gitlab/import_export/commit_mapper.rb diff --git a/lib/gitlab/import_export/commit_mapper.rb b/lib/gitlab/import_export/commit_mapper.rb new file mode 100644 index 00000000000..405149d3cd8 --- /dev/null +++ b/lib/gitlab/import_export/commit_mapper.rb @@ -0,0 +1,49 @@ +module Gitlab + module ImportExport + class CommitMapper + def initialize(commits:, members_map:, project_id:, relation_factory: Gitlab::ImportExport::RelationFactory, user_admin:) + @commits = commits + @members_map = members_map + @project_id = project_id + @relation_factory = relation_factory + @user_admin = user_admin + end + + def ids_map + @ids_map ||= map_commits + end + + def map_commits + @id_map = Hash.new(-1) + + @commits.each do |commit_hash| + @relation_factory.update_user_references(commit_hash, @members_map) + + commit_hash['project_id'] = @project_id + @relation_factory.update_project_references(commit_hash, Ci::Commit) + create_commit_statuses(commit_hash) + create_commit(commit_hash) + end + @id_map + end + + def create_commit(commit_hash) + old_id = commit_hash.delete('id') + commit = Ci::Commit.new(commit_hash) + commit.save! + @id_map[old_id] = commit.id + end + + def create_commit_statuses(commit_hash) + commit_hash['statuses'].map! do |status_hash| + @relation_factory.create(relation_sym: :statuses, + relation_hash: status_hash.merge('project_id' => @project_id, + 'commit_id' => nil), + members_mapper: @members_map, + commits_mapper: nil, + user_admin: @user_admin) + end + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 911ba06e748..750b69caafb 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -14,6 +14,7 @@ module Gitlab json = IO.read(@path) @tree_hash = ActiveSupport::JSON.decode(json) @project_members = @tree_hash.delete('project_members') + @commits = @tree_hash.delete('ci_commits') create_relations rescue => e @shared.error(e) @@ -32,6 +33,13 @@ module Gitlab project: project) end + def commits_mapper + @commits_mapper ||= Gitlab::ImportExport::CommitMapper.new(commits: @commits, + members_map: members_mapper.map, + project_id: project.id, + user_admin: @user.is_admin?) + end + def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) saved = [] relation_list.each do |relation| @@ -47,7 +55,7 @@ module Gitlab def default_relation_list Gitlab::ImportExport::ImportExportReader.new(shared: @shared).tree.reject do |model| - model.is_a?(Hash) && model[:project_members] + model.is_a?(Hash) && (model[:project_members] || model[:ci_commits]) end end @@ -65,9 +73,8 @@ module Gitlab relation_key = relation.keys.first.to_s tree_hash[relation_key].each do |relation_item| relation.values.flatten.each do |sub_relation| - if sub_relation.is_a?(Hash) - relation_hash = relation_item[sub_relation.keys.first.to_s] + relation_hash = relation_item[sub_relation.keys.first.to_s] sub_relation = sub_relation.keys.first else relation_hash = relation_item[sub_relation.to_s] @@ -97,6 +104,7 @@ module Gitlab Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_mapper: members_mapper, + commits_mapper: commits_mapper, user_admin: @user.is_admin?) end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 082398d1f0f..7f0f8f8077d 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -3,21 +3,27 @@ module Gitlab module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze - USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze - - def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) + OVERRIDES = { snippets: :project_snippets, + ci_commits: 'Ci::Commit', + statuses: 'commit_status', + variables: 'Ci::Variable', + triggers: 'Ci::Trigger', + builds: 'Ci::Build', + hooks: 'ProjectHook' }.freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id user_id).freeze + + def create(relation_sym:, relation_hash:, members_mapper:, commits_mapper:, user_admin:) relation_sym = parse_relation_sym(relation_sym) klass = parse_relation(relation_hash, relation_sym) update_missing_author(relation_hash, members_mapper, user_admin) if relation_sym == :notes update_user_references(relation_hash, members_mapper.map) update_project_references(relation_hash, klass) + update_commit_references(relation_hash, commits_mapper.ids_map) if commits_mapper - imported_object(klass, relation_hash) + generate_imported_object(klass, relation_hash, relation_sym) end - private def update_user_references(relation_hash, members_map) USER_REFERENCES.each do |reference| @@ -27,6 +33,32 @@ module Gitlab end end + def update_project_references(relation_hash, klass) + project_id = relation_hash.delete('project_id') + + if relation_hash['source_project_id'] && relation_hash['target_project_id'] + # If source and target are the same, populate them with the new project ID. + if relation_hash['target_project_id'] == relation_hash['source_project_id'] + relation_hash['source_project_id'] = project_id + else + relation_hash['source_project_id'] = -1 + end + end + relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + + # project_id may not be part of the export, but we always need to populate it if required. + relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') + relation_hash['gl_project_id'] = project_id if relation_hash['gl_project_id'] + end + + private + + def update_commit_references(relation_hash, commit_ids_map) + return unless relation_hash['commit_id'] + old_commit_id = relation_hash['commit_id'] + relation_hash['commit_id'] = commit_ids_map[old_commit_id] + end + def update_missing_author(relation_hash, members_map, user_admin) old_author_id = relation_hash['author_id'] @@ -50,22 +82,15 @@ module Gitlab "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" end - def update_project_references(relation_hash, klass) - project_id = relation_hash.delete('project_id') - - if relation_hash['source_project_id'] && relation_hash['target_project_id'] - # If source and target are the same, populate them with the new project ID. - if relation_hash['target_project_id'] == relation_hash['source_project_id'] - relation_hash['source_project_id'] = project_id - else - relation_hash['source_project_id'] = -1 + def generate_imported_object(klass, relation_hash, relation_sym) + if relation_sym == 'Ci::Build' # call #trace= method after assigning the other attributes + trace = relation_hash.delete('trace') + imported_object(klass, relation_hash) do |imported_object| + imported_object.trace = trace end + else + imported_object(klass, relation_hash) end - relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] - - # project_id may not be part of the export, but we always need to populate it if required. - relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') - relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] end def relation_class(relation_sym) @@ -78,6 +103,7 @@ module Gitlab def imported_object(klass, relation_hash) imported_object = klass.new(relation_hash) + yield(imported_object) if block_given? imported_object.importing = true if imported_object.respond_to?(:importing) imported_object end -- cgit v1.2.1 From 0df21ac712d7afd99b0d38a52e7375b11a4fe269 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 15:15:14 +0200 Subject: revert changes as builds are related to statuses which are already there --- lib/gitlab/import_export/commit_mapper.rb | 49 ----------------- lib/gitlab/import_export/project_tree_restorer.rb | 14 ++--- lib/gitlab/import_export/relation_factory.rb | 66 +++++++---------------- 3 files changed, 23 insertions(+), 106 deletions(-) delete mode 100644 lib/gitlab/import_export/commit_mapper.rb diff --git a/lib/gitlab/import_export/commit_mapper.rb b/lib/gitlab/import_export/commit_mapper.rb deleted file mode 100644 index 405149d3cd8..00000000000 --- a/lib/gitlab/import_export/commit_mapper.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Gitlab - module ImportExport - class CommitMapper - def initialize(commits:, members_map:, project_id:, relation_factory: Gitlab::ImportExport::RelationFactory, user_admin:) - @commits = commits - @members_map = members_map - @project_id = project_id - @relation_factory = relation_factory - @user_admin = user_admin - end - - def ids_map - @ids_map ||= map_commits - end - - def map_commits - @id_map = Hash.new(-1) - - @commits.each do |commit_hash| - @relation_factory.update_user_references(commit_hash, @members_map) - - commit_hash['project_id'] = @project_id - @relation_factory.update_project_references(commit_hash, Ci::Commit) - create_commit_statuses(commit_hash) - create_commit(commit_hash) - end - @id_map - end - - def create_commit(commit_hash) - old_id = commit_hash.delete('id') - commit = Ci::Commit.new(commit_hash) - commit.save! - @id_map[old_id] = commit.id - end - - def create_commit_statuses(commit_hash) - commit_hash['statuses'].map! do |status_hash| - @relation_factory.create(relation_sym: :statuses, - relation_hash: status_hash.merge('project_id' => @project_id, - 'commit_id' => nil), - members_mapper: @members_map, - commits_mapper: nil, - user_admin: @user_admin) - end - end - end - end -end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 750b69caafb..911ba06e748 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -14,7 +14,6 @@ module Gitlab json = IO.read(@path) @tree_hash = ActiveSupport::JSON.decode(json) @project_members = @tree_hash.delete('project_members') - @commits = @tree_hash.delete('ci_commits') create_relations rescue => e @shared.error(e) @@ -33,13 +32,6 @@ module Gitlab project: project) end - def commits_mapper - @commits_mapper ||= Gitlab::ImportExport::CommitMapper.new(commits: @commits, - members_map: members_mapper.map, - project_id: project.id, - user_admin: @user.is_admin?) - end - def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) saved = [] relation_list.each do |relation| @@ -55,7 +47,7 @@ module Gitlab def default_relation_list Gitlab::ImportExport::ImportExportReader.new(shared: @shared).tree.reject do |model| - model.is_a?(Hash) && (model[:project_members] || model[:ci_commits]) + model.is_a?(Hash) && model[:project_members] end end @@ -73,8 +65,9 @@ module Gitlab relation_key = relation.keys.first.to_s tree_hash[relation_key].each do |relation_item| relation.values.flatten.each do |sub_relation| + if sub_relation.is_a?(Hash) - relation_hash = relation_item[sub_relation.keys.first.to_s] + relation_hash = relation_item[sub_relation.keys.first.to_s] sub_relation = sub_relation.keys.first else relation_hash = relation_item[sub_relation.to_s] @@ -104,7 +97,6 @@ module Gitlab Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_mapper: members_mapper, - commits_mapper: commits_mapper, user_admin: @user.is_admin?) end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 7f0f8f8077d..082398d1f0f 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -3,27 +3,21 @@ module Gitlab module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets, - ci_commits: 'Ci::Commit', - statuses: 'commit_status', - variables: 'Ci::Variable', - triggers: 'Ci::Trigger', - builds: 'Ci::Build', - hooks: 'ProjectHook' }.freeze - USER_REFERENCES = %w(author_id assignee_id updated_by_id user_id).freeze - - def create(relation_sym:, relation_hash:, members_mapper:, commits_mapper:, user_admin:) + OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze + + def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) relation_sym = parse_relation_sym(relation_sym) klass = parse_relation(relation_hash, relation_sym) update_missing_author(relation_hash, members_mapper, user_admin) if relation_sym == :notes update_user_references(relation_hash, members_mapper.map) update_project_references(relation_hash, klass) - update_commit_references(relation_hash, commits_mapper.ids_map) if commits_mapper - generate_imported_object(klass, relation_hash, relation_sym) + imported_object(klass, relation_hash) end + private def update_user_references(relation_hash, members_map) USER_REFERENCES.each do |reference| @@ -33,32 +27,6 @@ module Gitlab end end - def update_project_references(relation_hash, klass) - project_id = relation_hash.delete('project_id') - - if relation_hash['source_project_id'] && relation_hash['target_project_id'] - # If source and target are the same, populate them with the new project ID. - if relation_hash['target_project_id'] == relation_hash['source_project_id'] - relation_hash['source_project_id'] = project_id - else - relation_hash['source_project_id'] = -1 - end - end - relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] - - # project_id may not be part of the export, but we always need to populate it if required. - relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') - relation_hash['gl_project_id'] = project_id if relation_hash['gl_project_id'] - end - - private - - def update_commit_references(relation_hash, commit_ids_map) - return unless relation_hash['commit_id'] - old_commit_id = relation_hash['commit_id'] - relation_hash['commit_id'] = commit_ids_map[old_commit_id] - end - def update_missing_author(relation_hash, members_map, user_admin) old_author_id = relation_hash['author_id'] @@ -82,15 +50,22 @@ module Gitlab "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" end - def generate_imported_object(klass, relation_hash, relation_sym) - if relation_sym == 'Ci::Build' # call #trace= method after assigning the other attributes - trace = relation_hash.delete('trace') - imported_object(klass, relation_hash) do |imported_object| - imported_object.trace = trace + def update_project_references(relation_hash, klass) + project_id = relation_hash.delete('project_id') + + if relation_hash['source_project_id'] && relation_hash['target_project_id'] + # If source and target are the same, populate them with the new project ID. + if relation_hash['target_project_id'] == relation_hash['source_project_id'] + relation_hash['source_project_id'] = project_id + else + relation_hash['source_project_id'] = -1 end - else - imported_object(klass, relation_hash) end + relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + + # project_id may not be part of the export, but we always need to populate it if required. + relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') + relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] end def relation_class(relation_sym) @@ -103,7 +78,6 @@ module Gitlab def imported_object(klass, relation_hash) imported_object = klass.new(relation_hash) - yield(imported_object) if block_given? imported_object.importing = true if imported_object.respond_to?(:importing) imported_object end -- cgit v1.2.1 From 6956fb6366d78dd979fb1046bcd8b05e6dfd2bf7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 15:22:26 +0200 Subject: update relation factory with new models exceptions --- lib/gitlab/import_export/relation_factory.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 082398d1f0f..68319441a63 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -3,7 +3,15 @@ module Gitlab module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status' }.freeze + + OVERRIDES = { snippets: :project_snippets, + ci_commits: 'Ci::Commit', + statuses: 'commit_status', + variables: 'Ci::Variable', + triggers: 'Ci::Trigger', + builds: 'Ci::Build', + hooks: 'ProjectHook' }.freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) -- cgit v1.2.1 From 301d64b84943f7f91588c330f2d22c172f529d52 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 15:23:32 +0200 Subject: updated import export conf --- lib/gitlab/import_export/import_export.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ae2bf677c90..947f3030f46 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -16,7 +16,6 @@ project_tree: - :merge_request_diff - ci_commits: - :statuses - - :builds - :variables - :triggers - :deploy_keys -- cgit v1.2.1 From a5f04ad48849b94aabeeb7450c6059e76372855c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 17:48:15 +0200 Subject: fixed CI commits on export --- lib/gitlab/import_export/attributes_finder.rb | 10 ++++++++-- lib/gitlab/import_export/import_export.yml | 6 +++++- lib/gitlab/import_export/import_export_reader.rb | 3 ++- spec/lib/gitlab/import_export/project_tree_saver_spec.rb | 9 +++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index 3439ef1006e..d230de781d5 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -2,9 +2,10 @@ module Gitlab module ImportExport class AttributesFinder - def initialize(included_attributes:, excluded_attributes:) + def initialize(included_attributes:, excluded_attributes:, methods:) @included_attributes = included_attributes || {} @excluded_attributes = excluded_attributes || {} + @methods = methods || {} end def find(model_object) @@ -27,10 +28,15 @@ module Gitlab @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] } end + def find_method(value) + key = key_from_hash(value) + @methods[key].nil? ? {} : { methods: @methods[key] } + end + private def find_attributes_only(value) - find_included(value).merge(find_excluded(value)) + find_included(value).merge(find_excluded(value)).merge(find_method(value)) end def key_from_hash(value) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 947f3030f46..eef4d92beee 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -45,4 +45,8 @@ included_attributes: # Do not include the following attributes for the models specified. excluded_attributes: snippets: - - :expired_at \ No newline at end of file + - :expired_at + +methods: + statuses: + - :type \ No newline at end of file diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index 25e13074e90..65d27f4b7a4 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -7,7 +7,8 @@ module Gitlab config_hash = YAML.load_file(config).deep_symbolize_keys @tree = config_hash[:project_tree] @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], - excluded_attributes: config_hash[:excluded_attributes]) + excluded_attributes: config_hash[:excluded_attributes], + methods: config_hash[:methods]) end def project_tree diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index b9cf88d3f77..1c55d0e33c1 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -84,10 +84,15 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do it 'has author on merge requests comments' do expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty end + it 'has commit statuses' do expect(saved_project_json['ci_commits'].first['statuses']).not_to be_empty end + it 'has CI builds' do + expect(saved_project_json['ci_commits'].first['statuses'].first['type']).to eq('Ci::Build') + end + it 'has ci commits' do expect(saved_project_json['ci_commits']).not_to be_empty end @@ -99,7 +104,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do merge_request = create(:merge_request) label = create(:label) snippet = create(:project_snippet) - commit_status = create(:commit_status) release = create(:release) project = create(:project, @@ -111,7 +115,8 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do releases: [release] ) - create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) + ci_commit = create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) + create(:ci_build, commit: ci_commit) create(:milestone, project: project) create(:note, noteable: issue) create(:note, noteable: merge_request) -- cgit v1.2.1 From 0de533ed08eb3e918f30c72a46635c256881d8c0 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 17:54:39 +0200 Subject: fix rubocop warning --- lib/gitlab/import_export/version_restorer.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gitlab/import_export/version_restorer.rb b/lib/gitlab/import_export/version_restorer.rb index 2c83bf08997..892352f5adf 100644 --- a/lib/gitlab/import_export/version_restorer.rb +++ b/lib/gitlab/import_export/version_restorer.rb @@ -34,4 +34,3 @@ module Gitlab end end end - -- cgit v1.2.1 From dac1d0d81250ef41e7a0a26edf54100229cbb22c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 18:15:28 +0200 Subject: updated test project file --- .../import_export/test_project_export.tar.gz | Bin 340727 -> 340523 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 380696d5ab5..7eed8a6306a 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From 6c082edef83c021b933724c33fb979efdc697904 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 18 May 2016 18:17:16 +0200 Subject: fixed issue exporting builds --- lib/gitlab/import_export/relation_factory.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 68319441a63..d4e49ae17f0 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -22,7 +22,7 @@ module Gitlab update_user_references(relation_hash, members_mapper.map) update_project_references(relation_hash, klass) - imported_object(klass, relation_hash) + generate_imported_object(klass, relation_hash, relation_sym) end private @@ -58,6 +58,18 @@ module Gitlab "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" end + def generate_imported_object(klass, relation_hash, relation_sym) + if relation_sym == 'commit_status' # call #trace= method after assigning the other attributes + trace = relation_hash.delete('trace') + imported_object(klass, relation_hash) do |imported_object| + imported_object.trace = trace + imported_object.commit_id = nil + end + else + imported_object(klass, relation_hash) + end + end + def update_project_references(relation_hash, klass) project_id = relation_hash.delete('project_id') @@ -86,6 +98,7 @@ module Gitlab def imported_object(klass, relation_hash) imported_object = klass.new(relation_hash) + yield(imported_object) if block_given? imported_object.importing = true if imported_object.respond_to?(:importing) imported_object end -- cgit v1.2.1 From 30f4dcd4c906a71db98833075c76eb59922f5b98 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 13:02:57 +0200 Subject: uploads export --- .../projects/import_export/export_service.rb | 6 +++- lib/gitlab/import_export/uploads_saver.rb | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 lib/gitlab/import_export/uploads_saver.rb diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 0691ca9d468..1a23a4ede97 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -4,7 +4,7 @@ module Projects def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) - save_all if [save_version, save_project_tree, bundle_repo, bundle_wiki_repo].all? + save_all if [save_version, save_project_tree, save_uploads, bundle_repo, bundle_wiki_repo].all? cleanup_and_notify_worker if @shared.errors.any? end @@ -18,6 +18,10 @@ module Projects Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save end + def save_uploads + Gitlab::ImportExport::UploadsSaver.save(project: project, shared: @shared) + end + def bundle_repo Gitlab::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle end diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb new file mode 100644 index 00000000000..3420d2ea4cb --- /dev/null +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -0,0 +1,35 @@ +module Gitlab + module ImportExport + class UploadsSaver + + def self.save(*args) + new(*args).save + end + + def initialize(project:, shared:) + @project = project + @shared = shared + end + + def save + return true unless File.directory?(uploads_path) + + FileUtils.copy_entry(uploads_path, uploads_export_path) + true + rescue => e + @shared.error(e.message) + false + end + + private + + def uploads_export_path + File.join(@shared.export_path, 'uploads') + end + + def uploads_path + File.join(Rails.root.join('public/uploads'), project.path_with_namespace) + end + end + end +end -- cgit v1.2.1 From bac27df16c35abb2f8115d9c94edcc4ecbace1a7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 13:10:41 +0200 Subject: Squashed commit of the following: commit 92de6309e1c918a4ae023641dc42b196b3fb25ea Merge: 6c082ed 30f4dcd Author: James Lopez Date: Thu May 19 13:06:34 2016 +0200 Merge branches 'feature/project-export' and 'feature/project-import' of gitlab.com:gitlab-org/gitlab-ce into feature/project-import commit 30f4dcd4c906a71db98833075c76eb59922f5b98 Author: James Lopez Date: Thu May 19 13:02:57 2016 +0200 uploads export --- .../projects/import_export/export_service.rb | 6 +++- lib/gitlab/import_export/uploads_saver.rb | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 lib/gitlab/import_export/uploads_saver.rb diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 0691ca9d468..1a23a4ede97 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -4,7 +4,7 @@ module Projects def execute(options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) - save_all if [save_version, save_project_tree, bundle_repo, bundle_wiki_repo].all? + save_all if [save_version, save_project_tree, save_uploads, bundle_repo, bundle_wiki_repo].all? cleanup_and_notify_worker if @shared.errors.any? end @@ -18,6 +18,10 @@ module Projects Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save end + def save_uploads + Gitlab::ImportExport::UploadsSaver.save(project: project, shared: @shared) + end + def bundle_repo Gitlab::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle end diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb new file mode 100644 index 00000000000..3420d2ea4cb --- /dev/null +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -0,0 +1,35 @@ +module Gitlab + module ImportExport + class UploadsSaver + + def self.save(*args) + new(*args).save + end + + def initialize(project:, shared:) + @project = project + @shared = shared + end + + def save + return true unless File.directory?(uploads_path) + + FileUtils.copy_entry(uploads_path, uploads_export_path) + true + rescue => e + @shared.error(e.message) + false + end + + private + + def uploads_export_path + File.join(@shared.export_path, 'uploads') + end + + def uploads_path + File.join(Rails.root.join('public/uploads'), project.path_with_namespace) + end + end + end +end -- cgit v1.2.1 From 816dfcb1c54277ce5e3e3da1a0c1028b4c2cdb3c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 13:21:56 +0200 Subject: fix path --- lib/gitlab/import_export/uploads_saver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb index 3420d2ea4cb..7bce4ddd4e5 100644 --- a/lib/gitlab/import_export/uploads_saver.rb +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -28,7 +28,7 @@ module Gitlab end def uploads_path - File.join(Rails.root.join('public/uploads'), project.path_with_namespace) + File.join(Rails.root.join('public/uploads'), @project.path_with_namespace) end end end -- cgit v1.2.1 From 1466997755f704b1f8af49ced136e91827a0892c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 15:36:20 +0200 Subject: import uploads. Fixed a few things to do with members, triggers, etc... --- app/models/member.rb | 9 +++++---- lib/gitlab/import_export.rb | 4 ++++ lib/gitlab/import_export/import_service.rb | 6 +++++- lib/gitlab/import_export/members_mapper.rb | 4 ++-- lib/gitlab/import_export/relation_factory.rb | 10 +++++++++- lib/gitlab/import_export/uploads_restorer.rb | 19 +++++++++++++++++++ lib/gitlab/import_export/uploads_saver.rb | 13 +++++++++---- 7 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 lib/gitlab/import_export/uploads_restorer.rb diff --git a/app/models/member.rb b/app/models/member.rb index cca82da89f1..bef0b545c70 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -22,6 +22,7 @@ class Member < ActiveRecord::Base include Gitlab::Access attr_accessor :raw_invite_token + attr_accessor :importing belongs_to :created_by, class_name: "User" belongs_to :user @@ -54,10 +55,10 @@ class Member < ActiveRecord::Base scope :owners, -> { where(access_level: OWNER) } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } - after_create :send_invite, if: :invite? - after_create :create_notification_setting, unless: :invite? - after_create :post_create_hook, unless: :invite? - after_update :post_update_hook, unless: :invite? + after_create :send_invite, if: :invite?, unless: :importing + after_create :create_notification_setting, unless: [:invite?, :importing] + after_create :post_create_hook, unless: [:invite?, :importing] + after_update :post_update_hook, unless: [:invite?, :importing] after_destroy :post_destroy_hook, unless: :invite? delegate :name, :username, :email, to: :user, prefix: true diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 65a8fcfadd0..9c04a3d6995 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -27,5 +27,9 @@ module Gitlab def version VERSION end + + def reset_tokens? + true + end end end diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index b025bff29bd..acd734f0890 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -16,7 +16,7 @@ module Gitlab def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, shared: @shared) - if [restore_version, restore_project_tree, restore_repo, restore_wiki_repo].all? + if [restore_version, restore_project_tree, restore_repo, restore_wiki_repo, restore_uploads].all? project_tree.project else project_tree.project.destroy if project_tree.project @@ -52,6 +52,10 @@ module Gitlab project: ProjectWiki.new(project_tree.project)).restore end + def restore_uploads + Gitlab::ImportExport::UploadsRestorer.restore(project: project_tree.project, shared: @shared) + end + def path_with_namespace(project_path) File.join(@namespace.path, project_path) end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 4401fab3d23..28e57867158 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -50,11 +50,11 @@ module Gitlab end def member_hash(member) - member.except('id').merge(source_id: @project.id) + member.except('id').merge(source_id: @project.id, importing: true) end def default_project_member_hash - { user: @user, access_level: ProjectMember::MASTER, source_id: @project.id } + { user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true } end def find_project_user_query(member) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index d4e49ae17f0..566777e8fc9 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -12,7 +12,7 @@ module Gitlab builds: 'Ci::Build', hooks: 'ProjectHook' }.freeze - USER_REFERENCES = %w(author_id assignee_id updated_by_id).freeze + USER_REFERENCES = %w(author_id assignee_id updated_by_id user_id).freeze def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) relation_sym = parse_relation_sym(relation_sym) @@ -21,6 +21,7 @@ module Gitlab update_missing_author(relation_hash, members_mapper, user_admin) if relation_sym == :notes update_user_references(relation_hash, members_mapper.map) update_project_references(relation_hash, klass) + reset_tokens(relation_hash) if relation_sym == 'Ci::Trigger' generate_imported_object(klass, relation_hash, relation_sym) end @@ -88,6 +89,13 @@ module Gitlab relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] end + def reset_tokens(relation_hash) + return unless Gitlab::ImportExport.reset_tokens? + + # If we import/export a project to the same instance, tokens will have to be reseated. + relation_hash['token'] = nil + end + def relation_class(relation_sym) relation_sym.to_s.classify.constantize end diff --git a/lib/gitlab/import_export/uploads_restorer.rb b/lib/gitlab/import_export/uploads_restorer.rb new file mode 100644 index 00000000000..fdcbc48eb1b --- /dev/null +++ b/lib/gitlab/import_export/uploads_restorer.rb @@ -0,0 +1,19 @@ +module Gitlab + module ImportExport + class UploadsRestorer < UploadsSaver + + class << self + alias_method :restore, :save + end + + def save + return true unless File.directory?(uploads_export_path) + + copy_files(uploads_export_path, uploads_path) + rescue => e + @shared.error(e) + false + end + end + end +end diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb index 3420d2ea4cb..93bc626b363 100644 --- a/lib/gitlab/import_export/uploads_saver.rb +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -14,21 +14,26 @@ module Gitlab def save return true unless File.directory?(uploads_path) - FileUtils.copy_entry(uploads_path, uploads_export_path) - true + copy_files(uploads_path, uploads_export_path) rescue => e - @shared.error(e.message) + @shared.error(e) false end private + def copy_files(source, destination) + FileUtils.mkdir_p(destination) + FileUtils.copy_entry(source, destination) + true + end + def uploads_export_path File.join(@shared.export_path, 'uploads') end def uploads_path - File.join(Rails.root.join('public/uploads'), project.path_with_namespace) + File.join(Rails.root.join('public/uploads'), @project.path_with_namespace) end end end -- cgit v1.2.1 From 7df495fb7702f3635d6746cb9b0d79d00eafed80 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 15:37:21 +0200 Subject: updated uploads saver --- lib/gitlab/import_export/uploads_saver.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb index 7bce4ddd4e5..93bc626b363 100644 --- a/lib/gitlab/import_export/uploads_saver.rb +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -14,15 +14,20 @@ module Gitlab def save return true unless File.directory?(uploads_path) - FileUtils.copy_entry(uploads_path, uploads_export_path) - true + copy_files(uploads_path, uploads_export_path) rescue => e - @shared.error(e.message) + @shared.error(e) false end private + def copy_files(source, destination) + FileUtils.mkdir_p(destination) + FileUtils.copy_entry(source, destination) + true + end + def uploads_export_path File.join(@shared.export_path, 'uploads') end -- cgit v1.2.1 From becd9bfdc6142779e3874ba0558af29d9ef96588 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 16:42:41 +0200 Subject: updated spec file --- .../import_export/test_project_export.tar.gz | Bin 340523 -> 672360 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 7eed8a6306a..b85055070d8 100644 Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ -- cgit v1.2.1 From dd86c91c730cfee6edbc6ed1708caa85056cea91 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 17:02:49 +0200 Subject: fixed small issue mapping members --- lib/gitlab/import_export/members_mapper.rb | 1 - lib/gitlab/import_export/relation_factory.rb | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 28e57867158..cd1e174f180 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -25,7 +25,6 @@ module Gitlab def default_project_member @default_project_member ||= begin - return @project.project_members.first.user.id unless @project.project_members.empty? default_member = ProjectMember.new(default_project_member_hash) default_member.save! default_member.user.id diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 566777e8fc9..d5b80beb312 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -39,11 +39,11 @@ module Gitlab def update_missing_author(relation_hash, members_map, user_admin) old_author_id = relation_hash['author_id'] - # Users with admin access have access to mapping of users + # Users with admin access can map users if user_admin - relation_hash['author_id'] = members_map.default_project_member - else relation_hash['author_id'] = members_map.map[old_author_id] + else + relation_hash['author_id'] = members_map.default_project_member end author = relation_hash.delete('author') -- cgit v1.2.1 From 6782cd3cf6bd298c0c26538db35704bf13fcabb3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 19 May 2016 17:05:35 +0200 Subject: fix extra space --- lib/gitlab/import_export/relation_factory.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index d5b80beb312..0fdf208cfa0 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -3,7 +3,6 @@ module Gitlab module RelationFactory extend self - OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', statuses: 'commit_status', -- cgit v1.2.1 From fa1884698db9d462ebff3301d42cbc7d43d62c49 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 20 May 2016 10:57:10 +0200 Subject: a few nice to have and updated changelog --- CHANGELOG | 1 + app/helpers/todos_helper.rb | 10 ++++++++-- app/models/project.rb | 2 ++ app/models/todo.rb | 5 +++++ app/views/projects/edit.html.haml | 2 +- lib/gitlab/import_export/import_service.rb | 13 +++++++++++++ 6 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 822fa4be565..992f782f803 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 8.8.0 (unreleased) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) - Expire repository exists? and has_visible_content? caches after a push if necessary - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) + - GitLab project import and export functionality v 8.7.4 - Fix always showing build notification message when switching between merge requests diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 2f066682180..5317b55520b 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -9,8 +9,12 @@ module TodosHelper def todo_action_name(todo) case todo.action - when Todo::ASSIGNED then 'assigned you' - when Todo::MENTIONED then 'mentioned you on' + when Todo::ASSIGNED then + 'assigned you' + when Todo::MENTIONED then + 'mentioned you on' + when Todo::IMPORTED then + 'imported successfully' end end @@ -27,6 +31,8 @@ module TodosHelper if todo.for_commit? namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, todo.target, anchor: anchor) + elsif todo.for_project? + namespace_project_path(todo.project.namespace, todo.project) else polymorphic_path([todo.project.namespace.becomes(Namespace), todo.project, todo.target], anchor: anchor) diff --git a/app/models/project.rb b/app/models/project.rb index 5003324f8f8..3bd5d048e10 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -94,6 +94,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + alias_attribute :title, :name + # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' diff --git a/app/models/todo.rb b/app/models/todo.rb index d85f7bfdf57..3333e56912a 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -19,6 +19,7 @@ class Todo < ActiveRecord::Base ASSIGNED = 1 MENTIONED = 2 + IMPORTED = 3 belongs_to :author, class_name: "User" belongs_to :note @@ -58,6 +59,10 @@ class Todo < ActiveRecord::Base target_type == "Commit" end + def for_project? + target_type == "Project" + end + # override to return commits, which are not active record def target if for_commit? diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 251fba474af..dcb46ad9d19 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -144,7 +144,7 @@ = link_to 'Generate new export', export_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" - = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), + = link_to 'Download latest export', download_export_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index acd734f0890..79794a4c34f 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -17,6 +17,7 @@ module Gitlab Gitlab::ImportExport::Importer.import(archive_file: @archive_file, shared: @shared) if [restore_version, restore_project_tree, restore_repo, restore_wiki_repo, restore_uploads].all? + Todo.create(attributes_for_todo) project_tree.project else project_tree.project.destroy if project_tree.project @@ -67,6 +68,18 @@ module Gitlab def wiki_repo_path File.join(@shared.export_path, 'project.wiki.bundle') end + + def attributes_for_todo + { user_id: @current_user.id, + project_id: project_tree.project.id, + target_type: 'Project', + target: project_tree.project, + action: Todo::IMPORTED, + author_id: @current_user.id, + state: :pending, + target_id: project_tree.project.id + } + end end end end -- cgit v1.2.1 From f70c000924c82bc579310fe784a6df159d15618b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 20 May 2016 11:56:02 +0200 Subject: fix indentation --- app/helpers/todos_helper.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 5317b55520b..25b812877e6 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -9,12 +9,12 @@ module TodosHelper def todo_action_name(todo) case todo.action - when Todo::ASSIGNED then - 'assigned you' - when Todo::MENTIONED then - 'mentioned you on' - when Todo::IMPORTED then - 'imported successfully' + when Todo::ASSIGNED then + 'assigned you' + when Todo::MENTIONED then + 'mentioned you on' + when Todo::IMPORTED then + 'imported successfully' end end -- cgit v1.2.1 From 7c8359b7441c2631ceec3bd3783a9761646a9f99 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 1 Jun 2016 18:03:51 +0200 Subject: started refactoring some stuff based on MR feedback --- app/models/concerns/importable.rb | 6 ++++ app/models/member.rb | 10 +++---- app/models/merge_request.rb | 7 ++--- app/models/merge_request_diff.rb | 5 ++-- db/schema.rb | 16 +++++------ lib/gitlab/import_export/members_mapper.rb | 34 +++++++++++++---------- lib/gitlab/import_export/project_tree_restorer.rb | 34 ++++++++--------------- lib/gitlab/import_export/relation_factory.rb | 17 +++++------- lib/gitlab/import_export/repo_restorer.rb | 1 - 9 files changed, 61 insertions(+), 69 deletions(-) create mode 100644 app/models/concerns/importable.rb diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb new file mode 100644 index 00000000000..019ef755849 --- /dev/null +++ b/app/models/concerns/importable.rb @@ -0,0 +1,6 @@ +module Importable + extend ActiveSupport::Concern + + attr_accessor :importing + alias_method :importing?, :importing +end diff --git a/app/models/member.rb b/app/models/member.rb index bef0b545c70..9a56d3d2181 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -19,10 +19,10 @@ class Member < ActiveRecord::Base include Sortable + include Importable include Gitlab::Access attr_accessor :raw_invite_token - attr_accessor :importing belongs_to :created_by, class_name: "User" belongs_to :user @@ -55,10 +55,10 @@ class Member < ActiveRecord::Base scope :owners, -> { where(access_level: OWNER) } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } - after_create :send_invite, if: :invite?, unless: :importing - after_create :create_notification_setting, unless: [:invite?, :importing] - after_create :post_create_hook, unless: [:invite?, :importing] - after_update :post_update_hook, unless: [:invite?, :importing] + after_create :send_invite, if: :invite?, unless: :importing? + after_create :create_notification_setting, unless: [:invite?, :importing?] + after_create :post_create_hook, unless: [:invite?, :importing?] + after_update :post_update_hook, unless: [:invite?, :importing?] after_destroy :post_destroy_hook, unless: :invite? delegate :name, :username, :email, to: :user, prefix: true diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 916329e04d6..c771968627c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -34,6 +34,7 @@ class MergeRequest < ActiveRecord::Base include Referable include Sortable include Taskable + include Importable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" @@ -48,8 +49,6 @@ class MergeRequest < ActiveRecord::Base delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil - attr_accessor :importing - # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests attr_accessor :allow_broken @@ -123,12 +122,12 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: [:allow_broken, :importing] + validates :source_project, presence: true, unless: [:allow_broken, :importing?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches, unless: [:allow_broken, :importing] + validate :validate_branches, unless: [:allow_broken, :importing?] validate :validate_fork scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 4278c7daa41..2c9eaf3849f 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -15,6 +15,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable + include Importable # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 @@ -37,9 +38,7 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_commits serialize :st_diffs - after_create :reload_content, unless: :importing - - attr_accessor :importing + after_create :reload_content, unless: :importing? def reload_content reload_commits diff --git a/db/schema.rb b/db/schema.rb index 71d953afe30..89dba1318fa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -70,11 +70,11 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 - t.boolean "akismet_enabled", default: false - t.string "akismet_api_key" t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false @@ -428,8 +428,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "updated_by_id" t.boolean "confidential", default: false t.datetime "deleted_at" - t.date "due_date" t.integer "moved_to_id" + t.date "due_date" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -716,8 +716,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.string "encrypted_credentials_iv" - t.string "encrypted_credentials_salt" + t.text "encrypted_credentials_iv" + t.text "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -815,9 +815,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "active", null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index cd1e174f180..9c47216eb8d 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -2,7 +2,7 @@ module Gitlab module ImportExport class MembersMapper - attr_reader :map, :note_member_list + attr_reader :note_member_list def initialize(exported_members:, user:, project:) @exported_members = exported_members @@ -19,33 +19,37 @@ module Gitlab default_project_member end - @map = generate_map end def default_project_member @default_project_member ||= begin default_member = ProjectMember.new(default_project_member_hash) - default_member.save! + default_member.create! default_member.user.id end end - private - - def generate_map - @exported_members.each do |member| - existing_user = User.where(find_project_user_query(member)).first - assign_member(existing_user, member) if existing_user - end - @project_member_map + def map + @map ||= + begin + @exported_members.inject(@project_member_map) do |hash, member| + existing_user = User.where(find_project_user_query(member)).first + if existing_user + old_user_id = member['user']['id'] + add_user_as_team_member(existing_user, member) + hash[old_user_id] = existing_user.id + end + hash + end + end end - def assign_member(existing_user, member) - old_user_id = member['user']['id'] + private + + def add_user_as_team_member(existing_user, member) member['user'] = existing_user - project_member = ProjectMember.new(member_hash(member)) - @project_member_map[old_user_id] = project_member.user.id if project_member.save + ProjectMember.create!(member_hash(member)) end def member_hash(member) diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 911ba06e748..e8f5fb88382 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -32,14 +32,15 @@ module Gitlab project: project) end - def create_relations(relation_list = default_relation_list, tree_hash = @tree_hash) + def create_relations saved = [] - relation_list.each do |relation| - next if !relation.is_a?(Hash) && tree_hash[relation.to_s].blank? - create_sub_relations(relation, tree_hash) if relation.is_a?(Hash) + default_relation_list.each do |relation| + next unless relation.is_a?(Hash) || @tree_hash[relation.to_s].present? + + create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash) relation_key = relation.is_a?(Hash) ? relation.keys.first : relation - relation_hash = create_relation(relation_key, tree_hash[relation_key.to_s]) + relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) saved << project.update_attribute(relation_key, relation_hash) end saved.all? @@ -73,32 +74,19 @@ module Gitlab relation_hash = relation_item[sub_relation.to_s] end - process_sub_relation(relation_hash, relation_item, sub_relation) unless relation_hash.blank? + relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? end end end - def process_sub_relation(relation_hash, relation_item, sub_relation) - if relation_hash.is_a?(Array) - sub_relation_object = create_relation(sub_relation, relation_hash) - else - sub_relation_object = relation_from_factory(sub_relation, relation_hash) - end - relation_item[sub_relation.to_s] = sub_relation_object - end - def create_relation(relation, relation_hash_list) [relation_hash_list].flatten.map do |relation_hash| - relation_from_factory(relation, relation_hash) + Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, + relation_hash: relation_hash.merge('project_id' => project.id), + members_mapper: members_mapper, + user_admin: @user.is_admin?) end end - - def relation_from_factory(relation, relation_hash) - Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, - relation_hash: relation_hash.merge('project_id' => project.id), - members_mapper: members_mapper, - user_admin: @user.is_admin?) - end end end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 0fdf208cfa0..2794eafe4b6 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -11,11 +11,14 @@ module Gitlab builds: 'Ci::Build', hooks: 'ProjectHook' }.freeze - USER_REFERENCES = %w(author_id assignee_id updated_by_id user_id).freeze + USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze + # Guesses a model and saves it to the DB given its name `relation_sym` def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) relation_sym = parse_relation_sym(relation_sym) - klass = parse_relation(relation_hash, relation_sym) + + klass = relation_class(relation_sym) + relation_hash.delete('id') update_missing_author(relation_hash, members_mapper, user_admin) if relation_sym == :notes update_user_references(relation_hash, members_mapper.map) @@ -49,8 +52,8 @@ module Gitlab return unless user_admin && members_map.note_member_list.include?(old_author_id) - relation_hash['note'] = ('*Blank note*') if relation_hash['note'].blank? - relation_hash['note'] += (missing_author_note(relation_hash['updated_at'], author['name'])) + relation_hash['note'] = '*Blank note*' if relation_hash['note'].blank? + relation_hash['note'] += missing_author_note(relation_hash['updated_at'], author['name']) end def missing_author_note(updated_at, author_name) @@ -109,12 +112,6 @@ module Gitlab imported_object.importing = true if imported_object.respond_to?(:importing) imported_object end - - def parse_relation(relation_hash, relation_sym) - klass = relation_class(relation_sym) - relation_hash.delete('id') - klass - end end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 36094b95aa4..9fac58e2242 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -12,7 +12,6 @@ module Gitlab def restore return true unless File.exists?(@path_to_bundle) - FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) -- cgit v1.2.1 From 102074c80152a0d9b808f3eea78a195234ef80bb Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 2 Jun 2016 10:59:54 +0200 Subject: more and more refactoring --- lib/gitlab/import_export/import_service.rb | 22 +++++----- lib/gitlab/import_export/members_mapper.rb | 49 ++++++++++++----------- lib/gitlab/import_export/project_tree_restorer.rb | 4 +- lib/gitlab/import_export/relation_factory.rb | 2 +- lib/gitlab/import_export/uploads_saver.rb | 4 -- lib/gitlab/import_export/version_checker.rb | 36 +++++++++++++++++ lib/gitlab/import_export/version_restorer.rb | 36 ----------------- 7 files changed, 74 insertions(+), 79 deletions(-) create mode 100644 lib/gitlab/import_export/version_checker.rb delete mode 100644 lib/gitlab/import_export/version_restorer.rb diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index acd734f0890..96cb8f4b174 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -16,7 +16,7 @@ module Gitlab def execute Gitlab::ImportExport::Importer.import(archive_file: @archive_file, shared: @shared) - if [restore_version, restore_project_tree, restore_repo, restore_wiki_repo, restore_uploads].all? + if check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore) project_tree.project else project_tree.project.destroy if project_tree.project @@ -26,12 +26,8 @@ module Gitlab private - def restore_version - Gitlab::ImportExport::VersionRestorer.restore(shared: @shared) - end - - def restore_project_tree - project_tree.restore + def check_version! + Gitlab::ImportExport::VersionChecker.check!(shared: @shared) end def project_tree @@ -40,20 +36,20 @@ module Gitlab namespace_id: @namespace.id) end - def restore_repo + def repo_restorer Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, shared: @shared, - project: project_tree.project).restore + project: project_tree.project) end - def restore_wiki_repo + def wiki_restorer Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, shared: @shared, - project: ProjectWiki.new(project_tree.project)).restore + project: ProjectWiki.new(project_tree.project)) end - def restore_uploads - Gitlab::ImportExport::UploadsRestorer.restore(project: project_tree.project, shared: @shared) + def uploads_restorer + Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.project, shared: @shared) end def path_with_namespace(project_path) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 9c47216eb8d..13fe68d5408 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -12,32 +12,27 @@ module Gitlab # This needs to run first, as second call would be from generate_map # which means project members already exist. - default_project_member - - @project_member_map = Hash.new do |_, key| - @note_member_list << key - default_project_member - end + ensure_default_member! + end + def map + @map ||= generate_map end - def default_project_member - @default_project_member ||= - begin - default_member = ProjectMember.new(default_project_member_hash) - default_member.create! - default_member.user.id - end + def default_user_id + @user.id end - def map + private + + + def generate_map @map ||= begin - @exported_members.inject(@project_member_map) do |hash, member| + @exported_members.inject(missing_keys_tracking_hash) do |hash, member| existing_user = User.where(find_project_user_query(member)).first - if existing_user - old_user_id = member['user']['id'] - add_user_as_team_member(existing_user, member) + old_user_id = member['user']['id'] + if existing_user && add_user_as_team_member(existing_user, member).persisted? hash[old_user_id] = existing_user.id end hash @@ -45,21 +40,27 @@ module Gitlab end end - private + def missing_keys_tracking_hash + Hash.new do |_, key| + @note_member_list << key + @user.id + end + end + + def ensure_default_member! + ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) + end def add_user_as_team_member(existing_user, member) member['user'] = existing_user - ProjectMember.create!(member_hash(member)) + + ProjectMember.create(member_hash(member)) end def member_hash(member) member.except('id').merge(source_id: @project.id, importing: true) end - def default_project_member_hash - { user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true } - end - def find_project_user_query(member) user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email'])) end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index e8f5fb88382..8d3a016ad71 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -80,12 +80,14 @@ module Gitlab end def create_relation(relation, relation_hash_list) - [relation_hash_list].flatten.map do |relation_hash| + relation_array = [relation_hash_list].flatten.map do |relation_hash| Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, relation_hash: relation_hash.merge('project_id' => project.id), members_mapper: members_mapper, user_admin: @user.is_admin?) end + + relation_hash_list.is_a?(Array) ? relation_array : relation_array.first end end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 2794eafe4b6..cecb4747822 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -45,7 +45,7 @@ module Gitlab if user_admin relation_hash['author_id'] = members_map.map[old_author_id] else - relation_hash['author_id'] = members_map.default_project_member + relation_hash['author_id'] = members_map.default_user_id end author = relation_hash.delete('author') diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb index 93bc626b363..7292e9d9712 100644 --- a/lib/gitlab/import_export/uploads_saver.rb +++ b/lib/gitlab/import_export/uploads_saver.rb @@ -2,10 +2,6 @@ module Gitlab module ImportExport class UploadsSaver - def self.save(*args) - new(*args).save - end - def initialize(project:, shared:) @project = project @shared = shared diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb new file mode 100644 index 00000000000..4f467760862 --- /dev/null +++ b/lib/gitlab/import_export/version_checker.rb @@ -0,0 +1,36 @@ +module Gitlab + module ImportExport + class VersionChecker + + def self.restore(*args) + new(*args).check + end + + def initialize(shared:) + @shared = shared + end + + def check! + version = File.open(version_file, &:readline) + verify_version!(version) + rescue => e + @shared.error(e) + false + end + + private + + def version_file + File.join(@shared.export_path, Gitlab::ImportExport.version_filename) + end + + def verify_version!(version) + if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version) + raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}") + else + true + end + end + end + end +end diff --git a/lib/gitlab/import_export/version_restorer.rb b/lib/gitlab/import_export/version_restorer.rb deleted file mode 100644 index 892352f5adf..00000000000 --- a/lib/gitlab/import_export/version_restorer.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Gitlab - module ImportExport - class VersionRestorer - - def self.restore(*args) - new(*args).restore - end - - def initialize(shared:) - @shared = shared - end - - def restore - version = File.open(version_file, &:readline) - verify_version!(version) - rescue => e - @shared.error(e) - false - end - - private - - def version_file - File.join(@shared.export_path, Gitlab::ImportExport.version_filename) - end - - def verify_version!(version) - if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version) - raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}") - else - true - end - end - end - end -end -- cgit v1.2.1 From a9fdf62b5797220b7736859a2bc0f34f96a7ed43 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 2 Jun 2016 14:07:09 +0200 Subject: refactoring relation factory, changed from module to class --- lib/gitlab/import_export/project_tree_restorer.rb | 5 + lib/gitlab/import_export/relation_factory.rb | 125 ++++++++++++---------- 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 8d3a016ad71..dc1e477a82f 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -32,6 +32,11 @@ module Gitlab project: project) end + # Loops through the tree of models defined in import_export.yml and + # finds them in the imported JSON so they can be instantiated and saved + # in the DB. The structure and relationships between models are guessed from + # the configuration yaml file too. + # Finally, it updates each attribute in the newly imported project. def create_relations saved = [] default_relation_list.each do |relation| diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index cecb4747822..3738dbaebd3 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -1,7 +1,6 @@ module Gitlab module ImportExport - module RelationFactory - extend self + class RelationFactory OVERRIDES = { snippets: :project_snippets, ci_commits: 'Ci::Commit', @@ -13,47 +12,54 @@ module Gitlab USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze - # Guesses a model and saves it to the DB given its name `relation_sym` - def create(relation_sym:, relation_hash:, members_mapper:, user_admin:) - relation_sym = parse_relation_sym(relation_sym) + def self.create(*args) + new(*args).create + end - klass = relation_class(relation_sym) - relation_hash.delete('id') + def initialize(relation_sym:, relation_hash:, members_mapper:, user_admin:) + @relation_name = OVERRIDES[relation_sym] || relation_sym + @relation_hash = relation_hash.except('id') + @members_mapper = members_mapper + @user_admin = user_admin + end - update_missing_author(relation_hash, members_mapper, user_admin) if relation_sym == :notes - update_user_references(relation_hash, members_mapper.map) - update_project_references(relation_hash, klass) - reset_tokens(relation_hash) if relation_sym == 'Ci::Trigger' + # Creates an object from an actual model with name "relation_sym" with params from + # the relation_hash, updating references with new object IDs, mapping users using + # the "members_mapper" object, also updating notes if required. + def create + set_note_author if @relation_name == :notes + update_user_references + update_project_references + reset_tokens if @relation_name == 'Ci::Trigger' - generate_imported_object(klass, relation_hash, relation_sym) + generate_imported_object end private - def update_user_references(relation_hash, members_map) + def update_user_references USER_REFERENCES.each do |reference| - if relation_hash[reference] - relation_hash[reference] = members_map[relation_hash[reference]] + if @relation_hash[reference] + @relation_hash[reference] = @members_mapper.map[@relation_hash[reference]] end end end - def update_missing_author(relation_hash, members_map, user_admin) - old_author_id = relation_hash['author_id'] + # Sets the author for a note. If the user importing the project + # has admin access, an actual mapping with new project members + # will be used. Otherwise, a note stating the original author name + # is left. + def set_note_author + old_author_id = @relation_hash['author_id'] # Users with admin access can map users - if user_admin - relation_hash['author_id'] = members_map.map[old_author_id] - else - relation_hash['author_id'] = members_map.default_user_id - end + @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id - author = relation_hash.delete('author') + author = @relation_hash.delete('author') - return unless user_admin && members_map.note_member_list.include?(old_author_id) - - relation_hash['note'] = '*Blank note*' if relation_hash['note'].blank? - relation_hash['note'] += missing_author_note(relation_hash['updated_at'], author['name']) + if admin_user? && @members_mapper.note_member_list.include?(old_author_id) + update_note_for_missing_author(author['name']) + end end def missing_author_note(updated_at, author_name) @@ -61,57 +67,60 @@ module Gitlab "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" end - def generate_imported_object(klass, relation_hash, relation_sym) - if relation_sym == 'commit_status' # call #trace= method after assigning the other attributes - trace = relation_hash.delete('trace') - imported_object(klass, relation_hash) do |imported_object| - imported_object.trace = trace - imported_object.commit_id = nil + def generate_imported_object + if @relation_sym == 'commit_status' # call #trace= method after assigning the other attributes + trace = @relation_hash.delete('trace') + imported_object do |object| + object.trace = trace + object.commit_id = nil end else - imported_object(klass, relation_hash) + imported_object end end - def update_project_references(relation_hash, klass) - project_id = relation_hash.delete('project_id') - - if relation_hash['source_project_id'] && relation_hash['target_project_id'] - # If source and target are the same, populate them with the new project ID. - if relation_hash['target_project_id'] == relation_hash['source_project_id'] - relation_hash['source_project_id'] = project_id - else - relation_hash['source_project_id'] = -1 - end - end - relation_hash['target_project_id'] = project_id if relation_hash['target_project_id'] + def update_project_references + project_id = @relation_hash.delete('project_id') # project_id may not be part of the export, but we always need to populate it if required. - relation_hash['project_id'] = project_id if klass.column_names.include?('project_id') - relation_hash['gl_project_id'] = project_id if relation_hash ['gl_project_id'] + @relation_hash['project_id'] = project_id if relation_class.column_names.include?('project_id') + @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id'] + @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id'] + @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id'] + + # If source and target are the same, populate them with the new project ID. + if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] && + @relation_hash['target_project_id'] == @relation_hash['source_project_id'] + @relation_hash['source_project_id'] = project_id + end end - def reset_tokens(relation_hash) + def reset_tokens return unless Gitlab::ImportExport.reset_tokens? # If we import/export a project to the same instance, tokens will have to be reseated. - relation_hash['token'] = nil - end - - def relation_class(relation_sym) - relation_sym.to_s.classify.constantize + @relation_hash['token'] = nil end - def parse_relation_sym(relation_sym) - OVERRIDES[relation_sym] || relation_sym + def relation_class + @relation_class ||= @relation_name.to_s.classify.constantize end - def imported_object(klass, relation_hash) - imported_object = klass.new(relation_hash) + def imported_object + imported_object = relation_class.new(@relation_hash) yield(imported_object) if block_given? imported_object.importing = true if imported_object.respond_to?(:importing) imported_object end + + def update_note_for_missing_author(author_name) + @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank? + @relation_hash['note'] += missing_author_note(@relation_hash['updated_at'], author_name) + end + + def admin_user? + @user_admin + end end end end -- cgit v1.2.1 From 41c06c311b9c5642f6056d0b34030980ebf507b3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 2 Jun 2016 14:44:59 +0200 Subject: refactoring more things based on MR feedback --- lib/gitlab/import_export/import_service.rb | 3 ++- lib/gitlab/import_export/relation_factory.rb | 6 +++--- lib/gitlab/import_export/repo_restorer.rb | 9 +++++++-- lib/gitlab/import_export/shared.rb | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/import_export/import_service.rb b/lib/gitlab/import_export/import_service.rb index 96cb8f4b174..db71f72efec 100644 --- a/lib/gitlab/import_export/import_service.rb +++ b/lib/gitlab/import_export/import_service.rb @@ -45,7 +45,8 @@ module Gitlab def wiki_restorer Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, shared: @shared, - project: ProjectWiki.new(project_tree.project)) + project: ProjectWiki.new(project_tree.project), + wiki: true) end def uploads_restorer diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 3738dbaebd3..dc86862c2d3 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -30,7 +30,7 @@ module Gitlab set_note_author if @relation_name == :notes update_user_references update_project_references - reset_tokens if @relation_name == 'Ci::Trigger' + reset_ci_tokens if @relation_name == 'Ci::Trigger' generate_imported_object end @@ -95,10 +95,10 @@ module Gitlab end end - def reset_tokens + def reset_ci_tokens return unless Gitlab::ImportExport.reset_tokens? - # If we import/export a project to the same instance, tokens will have to be reseated. + # If we import/export a project to the same instance, tokens will have to be reset. @relation_hash['token'] = nil end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 9fac58e2242..d6dcf5dc116 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -3,14 +3,15 @@ module Gitlab class RepoRestorer include Gitlab::ImportExport::CommandLineUtil - def initialize(project:, shared:, path_to_bundle:) + def initialize(project:, shared:, path_to_bundle:, wiki: false) @project = project @path_to_bundle = path_to_bundle @shared = shared + @wiki = wiki end def restore - return true unless File.exists?(@path_to_bundle) + return false unless File.exists?(@path_to_bundle) || wiki? FileUtils.mkdir_p(path_to_repo) @@ -29,6 +30,10 @@ module Gitlab def path_to_repo @project.repository.path_to_repo end + + def wiki? + @wiki + end end end end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 01ac332981b..6aff05b886a 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -10,7 +10,7 @@ module Gitlab end def export_path - @export_path ||= Gitlab::ImportExport.export_path(relative_path: @opts[:relative_path]) + @export_path ||= Gitlab::ImportExport.export_path(relative_path: opts[:relative_path]) end def error(error) -- cgit v1.2.1 From 3e99123095b26988de67a94b0e7a5207c1ef5ae2 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 3 Jun 2016 10:57:46 +0200 Subject: Fix merge conflicts - squashed commit # Conflicts: # app/models/project.rb --- .gitignore | 78 +-- .rubocop.yml | 273 +++++++---- .vagrant_enabled | 0 CHANGELOG | 154 +++++- CONTRIBUTING.md | 6 +- GITLAB_SHELL_VERSION | 2 +- GITLAB_WORKHORSE_VERSION | 2 +- Gemfile | 39 +- Gemfile.lock | 212 +++----- README.md | 6 +- VERSION | 2 +- app/assets/images/ci/arch.jpg | Bin 25222 -> 0 bytes app/assets/images/ci/favicon.ico | Bin 5430 -> 0 bytes app/assets/images/ci/loader.gif | Bin 4405 -> 0 bytes app/assets/images/ci/no_avatar.png | Bin 1337 -> 0 bytes app/assets/images/ci/rails.png | Bin 6646 -> 0 bytes app/assets/images/ci/service_sample.png | Bin 76024 -> 0 bytes app/assets/images/mailers/gitlab_header_logo.png | Bin 0 -> 7096 bytes app/assets/images/mailers/gitlab_tanuki_2x.png | Bin 0 -> 2545 bytes app/assets/javascripts/api.js.coffee | 35 +- app/assets/javascripts/application.js.coffee | 43 +- .../blob/blob_gitignore_selector.js.coffee | 58 +++ app/assets/javascripts/blob/edit_blob.js.coffee | 1 + app/assets/javascripts/calendar.js.coffee | 34 -- app/assets/javascripts/ci/application.js.coffee | 28 -- app/assets/javascripts/ci/build.coffee | 24 +- app/assets/javascripts/dispatcher.js.coffee | 3 +- app/assets/javascripts/due_date_select.js.coffee | 35 +- app/assets/javascripts/gfm_auto_complete.js.coffee | 19 + app/assets/javascripts/gl_dropdown.js.coffee | 82 +++- .../javascripts/graphs/application.js.coffee | 7 + app/assets/javascripts/graphs/stat_graph.js.coffee | 6 + .../graphs/stat_graph_contributors.js.coffee | 71 +++ .../graphs/stat_graph_contributors_graph.js.coffee | 169 +++++++ .../graphs/stat_graph_contributors_util.js.coffee | 98 ++++ app/assets/javascripts/issuable.js.coffee | 48 +- app/assets/javascripts/issuable_form.js.coffee | 26 + app/assets/javascripts/issues.js.coffee | 38 -- app/assets/javascripts/layout_nav.js.coffee | 14 + app/assets/javascripts/lib/type_utility.js.coffee | 9 + app/assets/javascripts/lib/url_utility.js.coffee | 11 +- .../javascripts/merge_request_tabs.js.coffee | 3 + .../javascripts/merge_request_widget.js.coffee | 15 +- app/assets/javascripts/notes.js.coffee | 12 +- app/assets/javascripts/right_sidebar.js.coffee | 36 +- .../javascripts/search_autocomplete.js.coffee | 22 +- .../shortcuts_dashboard_navigation.js.coffee | 8 +- app/assets/javascripts/shortcuts_issuable.coffee | 4 + app/assets/javascripts/sidebar.js.coffee | 2 +- app/assets/javascripts/stat_graph.js.coffee | 6 - .../javascripts/stat_graph_contributors.js.coffee | 72 --- .../stat_graph_contributors_graph.js.coffee | 171 ------- .../stat_graph_contributors_util.js.coffee | 98 ---- app/assets/javascripts/user_tabs.js.coffee | 9 +- app/assets/javascripts/users/application.js.coffee | 8 + app/assets/javascripts/users/calendar.js.coffee | 198 ++++++++ app/assets/javascripts/users_select.js.coffee | 12 +- app/assets/stylesheets/application.scss | 2 - app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/animations.scss | 72 +++ app/assets/stylesheets/framework/avatar.scss | 1 + app/assets/stylesheets/framework/blocks.scss | 11 +- app/assets/stylesheets/framework/buttons.scss | 18 +- app/assets/stylesheets/framework/calendar.scss | 72 +-- app/assets/stylesheets/framework/common.scss | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 6 +- app/assets/stylesheets/framework/files.scss | 31 +- app/assets/stylesheets/framework/forms.scss | 8 +- app/assets/stylesheets/framework/gitlab-theme.scss | 17 +- app/assets/stylesheets/framework/header.scss | 58 ++- app/assets/stylesheets/framework/mobile.scss | 4 - app/assets/stylesheets/framework/nav.scss | 122 ++++- app/assets/stylesheets/framework/sidebar.scss | 52 +- app/assets/stylesheets/framework/typography.scss | 8 + app/assets/stylesheets/framework/variables.scss | 15 +- app/assets/stylesheets/framework/zen.scss | 2 +- app/assets/stylesheets/mailers/devise.scss | 134 +++++ .../stylesheets/mailers/repository_push_email.scss | 43 ++ app/assets/stylesheets/pages/commit.scss | 24 +- app/assets/stylesheets/pages/detail_page.scss | 2 - app/assets/stylesheets/pages/editor.scss | 21 +- app/assets/stylesheets/pages/issuable.scss | 6 +- app/assets/stylesheets/pages/issues.scss | 10 - app/assets/stylesheets/pages/merge_requests.scss | 31 +- app/assets/stylesheets/pages/notes.scss | 3 +- app/assets/stylesheets/pages/pipelines.scss | 24 + app/assets/stylesheets/pages/profile.scss | 16 +- app/assets/stylesheets/pages/projects.scss | 170 +++++-- app/assets/stylesheets/pages/search.scss | 4 +- app/assets/stylesheets/pages/settings.scss | 8 + app/assets/stylesheets/pages/snippets.scss | 44 +- app/assets/stylesheets/pages/todos.scss | 14 +- app/assets/stylesheets/pages/tree.scss | 13 +- app/controllers/admin/abuse_reports_controller.rb | 2 +- .../admin/application_settings_controller.rb | 17 +- .../admin/broadcast_messages_controller.rb | 2 +- app/controllers/admin/health_check_controller.rb | 5 + app/controllers/admin/keys_controller.rb | 2 +- app/controllers/admin/runners_controller.rb | 37 +- app/controllers/admin/spam_logs_controller.rb | 2 +- app/controllers/admin/users_controller.rb | 3 +- app/controllers/application_controller.rb | 6 +- app/controllers/autocomplete_controller.rb | 18 + app/controllers/concerns/creates_commit.rb | 2 +- .../concerns/toggle_subscription_action.rb | 2 +- app/controllers/dashboard/labels_controller.rb | 2 +- app/controllers/dashboard/projects_controller.rb | 2 +- app/controllers/dashboard/todos_controller.rb | 4 +- app/controllers/dashboard_controller.rb | 2 +- app/controllers/groups/group_members_controller.rb | 2 +- app/controllers/health_check_controller.rb | 22 + app/controllers/jwt_controller.rb | 87 ++++ app/controllers/omniauth_callbacks_controller.rb | 2 +- app/controllers/profiles/emails_controller.rb | 2 +- app/controllers/profiles/keys_controller.rb | 2 +- app/controllers/projects/application_controller.rb | 2 +- app/controllers/projects/branches_controller.rb | 2 +- app/controllers/projects/builds_controller.rb | 8 + app/controllers/projects/commit_controller.rb | 12 +- app/controllers/projects/compare_controller.rb | 3 +- .../projects/container_registry_controller.rb | 34 ++ app/controllers/projects/find_file_controller.rb | 52 +- app/controllers/projects/hooks_controller.rb | 9 +- app/controllers/projects/imports_controller.rb | 1 + .../projects/merge_requests_controller.rb | 16 +- app/controllers/projects/milestones_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 10 +- app/controllers/projects/pipelines_controller.rb | 59 +++ .../projects/project_members_controller.rb | 4 +- .../projects/protected_branches_controller.rb | 2 +- app/controllers/projects/runners_controller.rb | 4 +- app/controllers/projects/variables_controller.rb | 30 +- app/controllers/projects/wikis_controller.rb | 4 +- app/controllers/projects_controller.rb | 16 +- app/controllers/registrations_controller.rb | 4 +- app/controllers/sessions_controller.rb | 2 + app/controllers/snippets_controller.rb | 2 +- app/controllers/users_controller.rb | 24 +- app/finders/group_projects_finder.rb | 2 +- app/finders/issuable_finder.rb | 10 +- app/finders/notes_finder.rb | 2 +- app/finders/pipelines_finder.rb | 38 ++ app/finders/todos_finder.rb | 2 +- app/helpers/application_helper.rb | 20 +- app/helpers/application_settings_helper.rb | 14 + app/helpers/auth_helper.rb | 10 + app/helpers/blob_helper.rb | 10 + app/helpers/button_helper.rb | 2 +- app/helpers/ci_status_helper.rb | 29 +- app/helpers/commits_helper.rb | 19 +- app/helpers/diff_helper.rb | 30 +- app/helpers/emails_helper.rb | 6 - app/helpers/events_helper.rb | 92 +--- app/helpers/gitlab_markdown_helper.rb | 2 +- app/helpers/gitlab_routing_helper.rb | 4 + app/helpers/groups_helper.rb | 2 +- app/helpers/issuables_helper.rb | 10 +- app/helpers/issues_helper.rb | 21 +- app/helpers/javascript_helper.rb | 7 + app/helpers/nav_helper.rb | 8 + app/helpers/notes_helper.rb | 29 +- app/helpers/projects_helper.rb | 21 +- app/helpers/search_helper.rb | 2 +- app/helpers/selects_helper.rb | 2 +- app/helpers/tab_helper.rb | 6 +- app/helpers/todos_helper.rb | 28 +- app/mailers/devise_mailer.rb | 2 + app/mailers/emails/projects.rb | 8 +- app/mailers/notify.rb | 2 + app/models/ability.rb | 54 +- app/models/abuse_report.rb | 12 - app/models/appearance.rb | 13 - app/models/application_setting.rb | 80 ++- app/models/audit_event.rb | 14 - app/models/broadcast_message.rb | 14 - app/models/ci/build.rb | 57 +-- app/models/ci/commit.rb | 50 +- app/models/ci/runner.rb | 39 +- app/models/ci/runner_project.rb | 12 - app/models/ci/trigger.rb | 13 - app/models/ci/trigger_request.rb | 12 - app/models/ci/variable.rb | 19 +- app/models/commit.rb | 23 +- app/models/commit_range.rb | 2 +- app/models/commit_status.rb | 54 +- app/models/concerns/issuable.rb | 57 ++- app/models/concerns/mentionable.rb | 19 +- app/models/concerns/participable.rb | 94 ++-- app/models/concerns/subscribable.rb | 6 + app/models/deploy_key.rb | 15 - app/models/deploy_keys_project.rb | 11 - app/models/email.rb | 11 - app/models/event.rb | 51 +- app/models/forked_project_link.rb | 11 - app/models/generic_commit_status.rb | 37 -- app/models/group.rb | 17 - app/models/hooks/project_hook.rb | 22 - app/models/hooks/service_hook.rb | 22 - app/models/hooks/system_hook.rb | 22 - app/models/hooks/web_hook.rb | 24 +- app/models/identity.rb | 12 - app/models/issue.rb | 47 +- app/models/key.rb | 17 +- app/models/label.rb | 18 +- app/models/label_link.rb | 12 - app/models/legacy_diff_note.rb | 157 ++++++ app/models/lfs_object.rb | 12 - app/models/lfs_objects_project.rb | 11 - app/models/member.rb | 19 - app/models/members/group_member.rb | 19 - app/models/members/project_member.rb | 26 +- app/models/merge_request.rb | 59 +-- app/models/merge_request_diff.rb | 45 +- app/models/milestone.rb | 91 +++- app/models/namespace.rb | 25 +- app/models/network/graph.rb | 35 +- app/models/note.rb | 269 +++------- app/models/notification_setting.rb | 13 - app/models/oauth_access_token.rb | 15 - app/models/personal_snippet.rb | 16 - app/models/project.rb | 161 +++--- app/models/project_group_link.rb | 12 - app/models/project_import_data.rb | 15 +- app/models/project_services/asana_service.rb | 24 - app/models/project_services/assembla_service.rb | 24 - app/models/project_services/bamboo_service.rb | 24 - app/models/project_services/buildkite_service.rb | 24 - .../project_services/builds_email_service.rb | 24 - app/models/project_services/campfire_service.rb | 24 - app/models/project_services/ci_service.rb | 24 - .../custom_issue_tracker_service.rb | 24 - app/models/project_services/drone_ci_service.rb | 24 - .../project_services/emails_on_push_service.rb | 24 - .../project_services/external_wiki_service.rb | 26 +- app/models/project_services/flowdock_service.rb | 24 - app/models/project_services/gemnasium_service.rb | 24 - app/models/project_services/gitlab_ci_service.rb | 24 - .../gitlab_issue_tracker_service.rb | 24 - app/models/project_services/hipchat_service.rb | 24 - app/models/project_services/irker_service.rb | 26 +- .../project_services/issue_tracker_service.rb | 24 - app/models/project_services/jira_service.rb | 24 - .../project_services/pivotaltracker_service.rb | 24 - app/models/project_services/pushover_service.rb | 24 - app/models/project_services/redmine_service.rb | 24 - app/models/project_services/slack_service.rb | 24 - .../slack_service/build_message.rb | 4 +- .../slack_service/issue_message.rb | 19 +- app/models/project_services/teamcity_service.rb | 24 - app/models/project_snippet.rb | 19 +- app/models/project_wiki.rb | 18 +- app/models/protected_branch.rb | 12 - app/models/release.rb | 12 - app/models/repository.rb | 56 ++- app/models/security_event.rb | 14 - app/models/sent_notification.rb | 14 - app/models/service.rb | 24 - app/models/snippet.rb | 23 +- app/models/spam_log.rb | 17 - app/models/subscription.rb | 13 - app/models/todo.rb | 27 +- app/models/user.rb | 110 ++--- app/models/users_star_project.rb | 11 - .../container_registry_authentication_service.rb | 87 ++++ app/services/ci/create_pipeline_service.rb | 50 ++ app/services/create_commit_builds_service.rb | 17 +- app/services/git_push_service.rb | 17 - app/services/git_tag_push_service.rb | 2 +- app/services/issues/update_service.rb | 10 + .../add_todo_when_build_fails_service.rb | 17 + app/services/merge_requests/base_service.rb | 25 + app/services/merge_requests/build_service.rb | 2 +- app/services/merge_requests/create_service.rb | 7 +- app/services/merge_requests/merge_service.rb | 8 +- .../merge_when_build_succeeds_service.rb | 23 +- app/services/merge_requests/refresh_service.rb | 7 + app/services/merge_requests/update_service.rb | 2 + app/services/notes/create_service.rb | 11 - app/services/projects/autocomplete_service.rb | 4 + app/services/projects/create_service.rb | 43 +- app/services/projects/destroy_service.rb | 12 +- app/services/projects/fork_service.rb | 14 +- app/services/projects/housekeeping_service.rb | 2 +- app/services/projects/import_service.rb | 2 +- app/services/projects/transfer_service.rb | 5 + app/services/system_hooks_service.rb | 2 +- app/services/system_note_service.rb | 25 +- app/services/todo_service.rb | 30 ++ app/services/wiki_pages/base_service.rb | 5 +- .../admin/abuse_reports/_abuse_report.html.haml | 2 +- .../admin/application_settings/_form.html.haml | 21 + app/views/admin/builds/index.html.haml | 3 +- app/views/admin/health_check/show.html.haml | 49 ++ app/views/admin/runners/_runner.html.haml | 12 +- app/views/admin/runners/show.html.haml | 24 +- app/views/dashboard/issues.atom.builder | 5 +- app/views/dashboard/projects/index.atom.builder | 4 +- app/views/dashboard/todos/_todo.html.haml | 14 +- .../mailer/confirmation_instructions.html.erb | 9 - .../mailer/confirmation_instructions.html.haml | 16 + .../mailer/confirmation_instructions.text.erb | 9 + app/views/devise/sessions/new.html.haml | 2 +- app/views/devise/sessions/two_factor.html.haml | 1 + app/views/devise/shared/_omniauth_box.html.haml | 2 +- app/views/doorkeeper/authorizations/new.html.haml | 2 +- app/views/events/_commit.html.haml | 2 +- app/views/events/_event.atom.builder | 20 + app/views/events/_event_issue.atom.haml | 2 +- app/views/events/_event_merge_request.atom.haml | 2 +- app/views/events/_event_note.atom.haml | 2 +- app/views/events/_event_push.atom.haml | 2 +- app/views/events/event/_common.html.haml | 2 +- app/views/events/event/_push.html.haml | 2 +- app/views/groups/_activities.html.haml | 2 +- app/views/groups/issues.atom.builder | 13 +- app/views/groups/show.atom.builder | 4 +- app/views/import/gitlab/status.html.haml | 2 +- app/views/issues/_issue.atom.builder | 14 + app/views/kaminari/gitlab/_first_page.html.haml | 2 +- app/views/kaminari/gitlab/_gap.html.haml | 2 +- app/views/kaminari/gitlab/_last_page.html.haml | 2 +- app/views/kaminari/gitlab/_next_page.html.haml | 2 +- app/views/kaminari/gitlab/_page.html.haml | 2 +- app/views/kaminari/gitlab/_paginator.html.haml | 6 +- app/views/kaminari/gitlab/_prev_page.html.haml | 2 +- app/views/layouts/_head.html.haml | 3 + app/views/layouts/_page.html.haml | 4 +- app/views/layouts/_search.html.haml | 7 +- app/views/layouts/devise_mailer.html.haml | 34 ++ app/views/layouts/header/_default.html.haml | 10 +- app/views/layouts/nav/_admin.html.haml | 5 + app/views/layouts/nav/_dashboard.html.haml | 14 +- app/views/layouts/nav/_group.html.haml | 75 +-- app/views/layouts/nav/_group_settings.html.haml | 2 +- app/views/layouts/nav/_profile.html.haml | 4 +- app/views/layouts/nav/_project.html.haml | 250 +++++----- app/views/layouts/nav/_project_settings.html.haml | 102 ++-- app/views/layouts/notify.html.haml | 1 + app/views/layouts/project.html.haml | 2 +- app/views/layouts/project_settings.html.haml | 3 +- app/views/notify/_note_message.html.haml | 2 +- app/views/notify/new_issue_email.html.haml | 2 +- app/views/notify/new_merge_request_email.html.haml | 2 +- .../notify/note_merge_request_email.html.haml | 4 +- app/views/notify/repository_push_email.html.haml | 59 ++- app/views/notify/repository_push_email.text.haml | 38 +- app/views/profiles/accounts/show.html.haml | 4 +- app/views/profiles/preferences/show.html.haml | 2 +- app/views/projects/_builds_settings.html.haml | 119 +++-- app/views/projects/_home_panel.html.haml | 86 ++-- app/views/projects/_md_preview.html.haml | 2 +- app/views/projects/_zen.html.haml | 2 +- app/views/projects/activity.html.haml | 1 - app/views/projects/artifacts/browse.html.haml | 1 - app/views/projects/badges/index.html.haml | 1 - app/views/projects/blame/show.html.haml | 1 - app/views/projects/blob/_editor.html.haml | 3 + app/views/projects/blob/_header_title.html.haml | 1 - app/views/projects/blob/edit.html.haml | 1 - app/views/projects/blob/new.html.haml | 1 - app/views/projects/blob/show.html.haml | 1 - app/views/projects/branches/destroy.js.haml | 1 - app/views/projects/branches/index.html.haml | 1 - app/views/projects/branches/new.html.haml | 1 - app/views/projects/builds/_header_title.html.haml | 1 - app/views/projects/builds/index.html.haml | 5 +- app/views/projects/builds/show.html.haml | 8 +- app/views/projects/buttons/_dropdown.html.haml | 2 +- app/views/projects/buttons/_fork.html.haml | 5 +- .../projects/buttons/_notifications.html.haml | 6 +- app/views/projects/ci/builds/_build.html.haml | 10 +- app/views/projects/ci/commits/_commit.html.haml | 71 +++ app/views/projects/commit/_builds.html.haml | 2 +- app/views/projects/commit/_ci_commit.html.haml | 63 +-- app/views/projects/commit/_ci_stage.html.haml | 15 + app/views/projects/commit/_commit_box.html.haml | 78 +-- app/views/projects/commit/builds.html.haml | 4 +- app/views/projects/commit/show.html.haml | 2 - app/views/projects/commits/_commit.atom.builder | 14 + app/views/projects/commits/_commit.html.haml | 4 +- app/views/projects/commits/_commits.html.haml | 2 +- app/views/projects/commits/_head.html.haml | 8 +- app/views/projects/commits/_header_title.html.haml | 1 - app/views/projects/commits/show.atom.builder | 15 +- app/views/projects/commits/show.html.haml | 1 - app/views/projects/compare/index.html.haml | 1 - app/views/projects/compare/show.html.haml | 1 - .../projects/container_registry/_tag.html.haml | 21 + .../projects/container_registry/index.html.haml | 39 ++ app/views/projects/deploy_keys/index.html.haml | 4 +- app/views/projects/diffs/_file.html.haml | 8 +- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 14 +- app/views/projects/diffs/_text_file.html.haml | 9 +- app/views/projects/edit.html.haml | 445 ++++++++--------- app/views/projects/empty.html.haml | 10 +- app/views/projects/find_file/show.html.haml | 1 - .../_generic_commit_status.html.haml | 15 +- app/views/projects/graphs/_head.html.haml | 1 + app/views/projects/graphs/_header_title.html.haml | 1 - app/views/projects/graphs/ci.html.haml | 1 - app/views/projects/graphs/commits.html.haml | 1 - app/views/projects/graphs/languages.html.haml | 1 - app/views/projects/graphs/show.html.haml | 3 +- app/views/projects/hooks/_project_hook.html.haml | 2 +- app/views/projects/hooks/index.html.haml | 9 +- app/views/projects/issues/_header_title.html.haml | 1 - app/views/projects/issues/_issue.html.haml | 16 +- .../projects/issues/_merge_requests.html.haml | 7 +- .../projects/issues/_related_branches.html.haml | 2 +- app/views/projects/issues/edit.html.haml | 1 - app/views/projects/issues/index.atom.builder | 4 +- app/views/projects/issues/index.html.haml | 1 - app/views/projects/issues/new.html.haml | 1 - app/views/projects/issues/show.html.haml | 5 +- app/views/projects/labels/_header_title.html.haml | 1 - app/views/projects/labels/edit.html.haml | 1 - app/views/projects/labels/index.html.haml | 3 +- app/views/projects/labels/new.html.haml | 1 - .../merge_requests/_header_title.html.haml | 1 - .../merge_requests/_merge_request.html.haml | 18 +- app/views/projects/merge_requests/_show.html.haml | 1 - .../merge_requests/dropdowns/_branch.html.haml | 2 +- app/views/projects/merge_requests/edit.html.haml | 1 - app/views/projects/merge_requests/index.html.haml | 1 - .../projects/merge_requests/invalid.html.haml | 1 - app/views/projects/merge_requests/new.html.haml | 1 - .../projects/merge_requests/show/_mr_box.html.haml | 4 +- .../projects/merge_requests/widget/_open.html.haml | 2 +- .../projects/merge_requests/widget/_show.html.haml | 8 +- .../merge_requests/widget/open/_accept.html.haml | 5 +- .../open/_merge_when_build_succeeds.html.haml | 5 +- .../widget/open/_not_allowed.html.haml | 4 +- .../projects/milestones/_header_title.html.haml | 1 - app/views/projects/milestones/edit.html.haml | 1 - app/views/projects/milestones/index.html.haml | 2 - app/views/projects/milestones/new.html.haml | 1 - app/views/projects/milestones/show.html.haml | 2 - app/views/projects/network/show.html.haml | 1 - app/views/projects/new.html.haml | 2 +- .../projects/notes/_commit_discussion.html.haml | 0 .../notes/_diff_notes_with_reply.html.haml | 18 +- .../_diff_notes_with_reply_parallel.html.haml | 20 +- app/views/projects/notes/_discussion.html.haml | 47 +- app/views/projects/notes/_form.html.haml | 1 + app/views/projects/notes/_note.html.haml | 11 +- app/views/projects/notes/_notes.html.haml | 9 +- .../projects/notes/discussions/_active.html.haml | 16 - .../projects/notes/discussions/_commit.html.haml | 25 - .../projects/notes/discussions/_diff.html.haml | 28 -- .../notes/discussions/_diff_with_notes.html.haml | 30 ++ .../projects/notes/discussions/_notes.html.haml | 7 + .../projects/notes/discussions/_outdated.html.haml | 14 - app/views/projects/pipelines/_head.html.haml | 14 + app/views/projects/pipelines/_info.html.haml | 37 ++ app/views/projects/pipelines/index.html.haml | 58 +++ app/views/projects/pipelines/new.html.haml | 21 + app/views/projects/pipelines/show.html.haml | 8 + .../project_members/_header_title.html.haml | 1 - .../projects/project_members/import.html.haml | 1 - app/views/projects/project_members/index.html.haml | 1 - .../protected_branches/_branches_list.html.haml | 2 +- app/views/projects/releases/edit.html.haml | 1 - app/views/projects/repositories/_feed.html.haml | 2 +- app/views/projects/runners/_form.html.haml | 32 ++ app/views/projects/runners/_runner.html.haml | 2 +- .../projects/runners/_specific_runners.html.haml | 2 +- app/views/projects/runners/edit.html.haml | 27 +- app/views/projects/runners/show.html.haml | 51 +- app/views/projects/services/_form.html.haml | 32 +- app/views/projects/services/index.html.haml | 52 +- app/views/projects/show.atom.builder | 4 +- app/views/projects/show.html.haml | 78 +-- app/views/projects/snippets/_actions.html.haml | 38 +- .../projects/snippets/_header_title.html.haml | 1 - app/views/projects/snippets/edit.html.haml | 1 - app/views/projects/snippets/index.html.haml | 1 - app/views/projects/snippets/new.html.haml | 1 - app/views/projects/snippets/show.html.haml | 9 +- app/views/projects/tags/destroy.js.haml | 1 - app/views/projects/tags/index.html.haml | 1 - app/views/projects/tags/new.html.haml | 3 +- app/views/projects/tags/show.html.haml | 9 +- app/views/projects/tree/show.html.haml | 2 +- app/views/projects/triggers/index.html.haml | 22 +- app/views/projects/variables/_content.html.haml | 8 + app/views/projects/variables/_form.html.haml | 10 + app/views/projects/variables/_table.html.haml | 25 + app/views/projects/variables/index.html.haml | 17 + app/views/projects/variables/show.html.haml | 41 +- app/views/projects/wikis/_header_title.html.haml | 1 - app/views/projects/wikis/edit.html.haml | 1 - app/views/projects/wikis/empty.html.haml | 1 - app/views/projects/wikis/git_access.html.haml | 1 - app/views/projects/wikis/history.html.haml | 1 - app/views/projects/wikis/pages.html.haml | 1 - app/views/projects/wikis/show.html.haml | 1 - app/views/search/results/_issue.html.haml | 2 +- app/views/search/results/_merge_request.html.haml | 2 +- app/views/search/results/_note.html.haml | 2 +- app/views/shared/_clone_panel.html.haml | 2 +- app/views/shared/_event_filter.html.haml | 4 +- app/views/shared/_sort_dropdown.html.haml | 2 +- app/views/shared/groups/_list.html.haml | 2 +- app/views/shared/issuable/_filter.html.haml | 6 +- app/views/shared/issuable/_form.html.haml | 93 ++-- app/views/shared/issuable/_search_form.html.haml | 6 - app/views/shared/issuable/_sidebar.html.haml | 24 +- .../shared/milestones/_participants_tab.html.haml | 2 +- app/views/shared/projects/_project.html.haml | 5 +- app/views/shared/snippets/_header.html.haml | 23 +- app/views/snippets/_actions.html.haml | 38 +- app/views/snippets/show.html.haml | 7 +- app/views/users/calendar.html.haml | 19 +- app/views/users/calendar_activities.html.haml | 42 +- app/views/users/show.atom.builder | 4 +- app/views/users/show.html.haml | 14 +- app/workers/emails_on_push_worker.rb | 44 +- app/workers/repository_fork_worker.rb | 6 +- app/workers/repository_import_worker.rb | 3 +- config/application.rb | 32 +- config/boot.rb | 2 +- config/environments/development.rb | 1 + config/environments/test.rb | 2 +- config/gitlab.yml.example | 10 + config/initializers/1_settings.rb | 46 +- config/initializers/5_backend.rb | 6 +- config/initializers/carrierwave.rb | 4 +- config/initializers/devise.rb | 2 +- config/initializers/devise_async.rb | 1 - config/initializers/doorkeeper.rb | 4 +- config/initializers/health_check.rb | 3 + config/initializers/metrics.rb | 3 + config/initializers/monkey_patch.rb | 48 -- config/initializers/omniauth.rb | 2 +- config/initializers/premailer.rb | 2 +- config/initializers/session_store.rb | 2 +- config/routes.rb | 63 ++- ...152808_remove_wrong_import_url_from_projects.rb | 4 +- ...ult_group_visibility_to_application_settings.rb | 4 +- ...0160407120251_add_images_enabled_for_project.rb | 5 + ...auth_sign_in_sources_to_application_settings.rb | 5 + ...20160504112519_add_run_untagged_to_ci_runner.rb | 13 + db/migrate/20160508215820_add_type_to_notes.rb | 5 + ...20160508221410_set_type_on_legacy_diff_notes.rb | 5 + ...h_check_access_token_to_application_settings.rb | 5 + ...r_confirmation_email_to_application_settings.rb | 12 + ...525205328_remove_main_language_from_projects.rb | 21 + ...e_notification_settings_for_deleted_projects.rb | 13 + db/migrate/20160528043124_add_users_state_index.rb | 9 + ...y_token_expire_delay_to_application_settings.rb | 9 + db/schema.rb | 55 ++- doc/README.md | 3 + doc/administration/container_registry.md | 375 ++++++++++++++ doc/administration/environment_variables.md | 2 +- doc/administration/high_availability/README.md | 4 + .../high_availability/load_balancer.md | 2 +- doc/administration/high_availability/nfs.md | 6 +- doc/administration/high_availability/redis.md | 2 +- .../high_availability/active-active-diagram.png | Bin 0 -> 29607 bytes .../high_availability/active-passive-diagram.png | Bin 0 -> 24246 bytes doc/administration/repository_checks.md | 4 +- doc/administration/troubleshooting/sidekiq.md | 8 + doc/api/README.md | 2 +- doc/api/build_triggers.md | 12 +- doc/api/groups.md | 2 - doc/api/issues.md | 15 +- doc/api/labels.md | 72 ++- doc/api/merge_requests.md | 22 +- doc/api/notes.md | 10 +- doc/api/projects.md | 3 + doc/api/runners.md | 4 +- doc/api/services.md | 8 +- doc/api/settings.md | 7 +- doc/ci/deployment/README.md | 98 ---- doc/ci/examples/deployment/README.md | 98 ++++ doc/ci/quick_start/README.md | 4 +- doc/ci/runners/README.md | 8 +- doc/ci/triggers/README.md | 2 +- doc/ci/yaml/README.md | 4 +- doc/container_registry/README.md | 113 +++++ doc/container_registry/img/container_registry.png | Bin 0 -> 354050 bytes doc/container_registry/img/project_feature.png | Bin 0 -> 392842 bytes doc/development/doc_styleguide.md | 4 +- doc/development/instrumentation.md | 129 ++++- doc/development/migration_style_guide.md | 48 +- doc/development/testing.md | 2 +- doc/development/ui_guide.md | 48 ++ doc/gitlab-basics/create-issue.md | 2 +- doc/gitlab-basics/create-project.md | 2 +- doc/hooks/custom_hooks.md | 2 +- doc/install/installation.md | 8 +- doc/install/relative_url.md | 2 +- doc/install/requirements.md | 3 +- doc/integration/README.md | 2 +- doc/integration/cas.md | 19 +- doc/integration/google.md | 4 +- .../img/enabled-oauth-sign-in-sources.png | Bin 0 -> 49081 bytes doc/integration/omniauth.md | 15 + doc/intro/README.md | 2 +- doc/logs/logs.md | 8 +- doc/markdown/markdown.md | 49 +- doc/migrate_ci_to_ce/README.md | 2 +- doc/monitoring/health_check.md | 66 +++ doc/monitoring/img/health_check_token.png | Bin 0 -> 10884 bytes doc/operations/moving_repositories.md | 8 +- doc/permissions/permissions.md | 3 + doc/raketasks/README.md | 2 +- doc/security/README.md | 1 + doc/security/user_email_confirmation.md | 7 + doc/update/8.6-to-8.7.md | 8 + doc/update/8.7-to-8.8.md | 162 ++++++ doc/update/README.md | 6 +- doc/update/patch_versions.md | 4 +- doc/web_hooks/web_hooks.md | 68 +++ doc/workflow/gitlab_flow.md | 4 +- doc/workflow/groups.md | 2 +- .../importing/import_projects_from_github.md | 2 +- .../importing/import_projects_from_gitlab_com.md | 2 +- .../lfs/manage_large_binaries_with_git_lfs.md | 6 +- doc/workflow/notifications.md | 2 +- features/project/active_tab.feature | 55 +-- features/project/builds/summary.feature | 1 + features/project/commits/tags.feature | 46 -- features/project/create.feature | 16 +- features/project/project.feature | 9 - features/project/shortcuts.feature | 8 +- features/steps/admin/active_tab.rb | 10 +- features/steps/admin/users.rb | 2 +- features/steps/dashboard/active_tab.rb | 6 +- features/steps/dashboard/dashboard.rb | 2 +- features/steps/dashboard/issues.rb | 2 +- features/steps/dashboard/merge_requests.rb | 2 +- features/steps/dashboard/shortcuts.rb | 3 +- features/steps/dashboard/todos.rb | 4 +- features/steps/profile/active_tab.rb | 4 - features/steps/profile/profile.rb | 2 +- features/steps/project/active_tab.rb | 32 +- features/steps/project/builds/summary.rb | 4 + features/steps/project/commits/commits.rb | 6 +- features/steps/project/commits/tags.rb | 90 ---- features/steps/project/create.rb | 26 +- features/steps/project/fork.rb | 2 +- features/steps/project/hooks.rb | 2 +- features/steps/project/issues/award_emoji.rb | 4 +- features/steps/project/issues/issues.rb | 6 +- features/steps/project/issues/labels.rb | 4 +- features/steps/project/merge_requests.rb | 10 +- features/steps/project/project.rb | 12 +- features/steps/project/project_find_file.rb | 4 +- features/steps/project/project_milestone.rb | 2 +- features/steps/project/project_shortcuts.rb | 1 + features/steps/project/snippets.rb | 4 +- features/steps/project/source/browse_files.rb | 16 +- features/steps/project/team_management.rb | 2 +- features/steps/shared/active_tab.rb | 28 +- features/steps/shared/diff_note.rb | 12 +- features/steps/shared/issuable.rb | 8 +- features/steps/shared/note.rb | 2 +- features/steps/shared/project.rb | 2 +- features/steps/shared/project_tab.rb | 20 +- features/steps/shared/shortcuts.rb | 2 +- features/steps/shared/sidebar_active_tab.rb | 35 ++ features/steps/snippets/snippets.rb | 4 +- features/steps/user.rb | 2 +- .../migration/create_table_migration.rb | 35 ++ .../active_record/migration/migration.rb | 55 +++ lib/api/api.rb | 71 +-- lib/api/api_guard.rb | 270 +++++----- lib/api/commit_statuses.rb | 2 +- lib/api/commits.rb | 2 + lib/api/entities.rb | 27 +- lib/api/gitignores.rb | 29 ++ lib/api/groups.rb | 3 +- lib/api/helpers.rb | 15 +- lib/api/issues.rb | 39 +- lib/api/labels.rb | 6 +- lib/api/licenses.rb | 14 +- lib/api/merge_requests.rb | 36 -- lib/api/notes.rb | 47 +- lib/api/projects.rb | 7 +- lib/api/runners.rb | 2 +- lib/api/subscriptions.rb | 60 +++ lib/api/users.rb | 2 +- lib/backup/manager.rb | 17 +- lib/backup/registry.rb | 13 + lib/banzai/filter/abstract_reference_filter.rb | 12 +- lib/banzai/filter/commit_range_reference_filter.rb | 20 +- lib/banzai/filter/commit_reference_filter.rb | 20 +- .../filter/external_issue_reference_filter.rb | 14 +- lib/banzai/filter/inline_diff_filter.rb | 26 + lib/banzai/filter/issue_reference_filter.rb | 7 +- lib/banzai/filter/label_reference_filter.rb | 2 + .../filter/merge_request_reference_filter.rb | 2 + lib/banzai/filter/milestone_reference_filter.rb | 46 +- lib/banzai/filter/redactor_filter.rb | 31 +- lib/banzai/filter/reference_filter.rb | 24 +- lib/banzai/filter/reference_gatherer_filter.rb | 65 --- lib/banzai/filter/sanitization_filter.rb | 2 +- lib/banzai/filter/snippet_reference_filter.rb | 2 + lib/banzai/filter/upload_link_filter.rb | 8 +- lib/banzai/filter/user_reference_filter.rb | 44 +- lib/banzai/filter/wiki_link_filter.rb | 11 +- lib/banzai/lazy_reference.rb | 25 - lib/banzai/pipeline/gfm_pipeline.rb | 3 +- .../pipeline/reference_extraction_pipeline.rb | 11 - lib/banzai/reference_extractor.rb | 48 +- lib/banzai/reference_parser.rb | 14 + lib/banzai/reference_parser/base_parser.rb | 204 ++++++++ lib/banzai/reference_parser/commit_parser.rb | 34 ++ lib/banzai/reference_parser/commit_range_parser.rb | 38 ++ .../reference_parser/external_issue_parser.rb | 25 + lib/banzai/reference_parser/issue_parser.rb | 40 ++ lib/banzai/reference_parser/label_parser.rb | 11 + .../reference_parser/merge_request_parser.rb | 11 + lib/banzai/reference_parser/milestone_parser.rb | 11 + lib/banzai/reference_parser/snippet_parser.rb | 11 + lib/banzai/reference_parser/user_parser.rb | 92 ++++ lib/ci/ansi2html.rb | 87 +++- lib/ci/api/api.rb | 10 +- lib/ci/api/runners.rb | 18 +- lib/ci/charts.rb | 3 +- lib/ci/gitlab_ci_yaml_processor.rb | 4 +- lib/container_registry/blob.rb | 48 ++ lib/container_registry/client.rb | 61 +++ lib/container_registry/config.rb | 16 + lib/container_registry/registry.rb | 21 + lib/container_registry/repository.rb | 48 ++ lib/container_registry/tag.rb | 77 +++ lib/event_filter.rb | 2 +- lib/gitlab.rb | 2 +- lib/gitlab/backend/shell.rb | 2 +- lib/gitlab/bitbucket_import/client.rb | 2 +- lib/gitlab/bitbucket_import/project_creator.rb | 7 +- lib/gitlab/ci/build/artifacts/metadata.rb | 2 +- lib/gitlab/contributions_calendar.rb | 2 +- lib/gitlab/current_settings.rb | 23 +- lib/gitlab/database.rb | 4 +- lib/gitlab/database/migration_helpers.rb | 142 ++++++ lib/gitlab/diff/inline_diff_marker.rb | 36 +- lib/gitlab/diff/parser.rb | 16 +- lib/gitlab/email/message/repository_push.rb | 11 +- lib/gitlab/email/reply_parser.rb | 2 +- lib/gitlab/fogbugz_import/project_creator.rb | 9 +- lib/gitlab/github_import/branch_formatter.rb | 29 ++ lib/gitlab/github_import/importer.rb | 76 ++- lib/gitlab/github_import/pull_request_formatter.rb | 54 +- lib/gitlab/gitignore.rb | 56 +++ lib/gitlab/gitlab_import/importer.rb | 8 +- lib/gitlab/google_code_import/project_creator.rb | 9 +- lib/gitlab/import_url.rb | 41 -- lib/gitlab/lazy.rb | 34 ++ lib/gitlab/markup_helper.rb | 2 +- lib/gitlab/metrics/instrumentation.rb | 2 - lib/gitlab/metrics/subscribers/rails_cache.rb | 12 +- lib/gitlab/middleware/go.rb | 2 +- lib/gitlab/middleware/rails_queue_duration.rb | 24 + lib/gitlab/project_search_results.rb | 2 +- lib/gitlab/redis.rb | 8 +- lib/gitlab/reference_extractor.rb | 19 +- lib/gitlab/regex.rb | 4 + lib/gitlab/sanitizers/svg.rb | 8 +- lib/gitlab/sanitizers/svg/whitelist.rb | 170 +++---- lib/gitlab/url_builder.rb | 2 +- lib/gitlab/url_sanitizer.rb | 54 ++ lib/gitlab/visibility_level.rb | 7 + lib/json_web_token/rsa_token.rb | 42 ++ lib/json_web_token/token.rb | 46 ++ lib/support/nginx/registry-ssl | 53 ++ lib/tasks/auto_annotate_models.rake | 44 -- lib/tasks/gitlab/backup.rake | 29 ++ lib/tasks/gitlab/check.rake | 2 +- lib/tasks/gitlab/db.rake | 14 +- lib/tasks/gitlab/update_gitignore.rake | 46 ++ lib/tasks/rubocop.rake | 1 + public/robots.txt | 1 + shared/registry/.gitkeep | 0 spec/config/mail_room_spec.rb | 2 +- spec/controllers/admin/projects_controller_spec.rb | 2 +- spec/controllers/admin/users_controller_spec.rb | 76 +++ spec/controllers/health_check_controller_spec.rb | 105 ++++ .../projects/branches_controller_spec.rb | 4 - .../projects/compare_controller_spec.rb | 4 +- .../projects/group_links_controller_spec.rb | 4 +- .../controllers/projects/issues_controller_spec.rb | 2 +- .../projects/merge_requests_controller_spec.rb | 2 +- .../notification_settings_controller_spec.rb | 14 + .../projects/project_members_controller_spec.rb | 2 +- spec/controllers/projects/raw_controller_spec.rb | 2 +- spec/controllers/projects_controller_spec.rb | 45 ++ spec/controllers/registrations_controller_spec.rb | 33 ++ spec/controllers/sessions_controller_spec.rb | 29 +- spec/controllers/users_controller_spec.rb | 22 + spec/factories/abuse_reports.rb | 12 - spec/factories/broadcast_messages.rb | 14 - spec/factories/forked_project_links.rb | 11 - spec/factories/label_links.rb | 12 - spec/factories/labels.rb | 13 - spec/factories/lfs_objects.rb | 12 - spec/factories/lfs_objects_projects.rb | 11 - spec/factories/merge_requests.rb | 29 -- spec/factories/notes.rb | 45 +- spec/factories/oauth_access_tokens.rb | 15 - spec/factories/projects.rb | 49 +- spec/factories/releases.rb | 12 - spec/factories/todos.rb | 22 +- spec/factories_spec.rb | 4 +- spec/features/admin/admin_builds_spec.rb | 1 + spec/features/admin/admin_health_check_spec.rb | 55 +++ spec/features/admin/admin_runners_spec.rb | 2 +- spec/features/admin/admin_users_spec.rb | 5 +- spec/features/builds_spec.rb | 4 +- spec/features/commits_spec.rb | 10 +- spec/features/container_registry_spec.rb | 44 ++ spec/features/issues/filter_issues_spec.rb | 176 +++++++ spec/features/issues/move_spec.rb | 16 +- spec/features/issues/note_polling_spec.rb | 5 +- spec/features/issues/update_issues_spec.rb | 2 +- spec/features/issues_spec.rb | 99 +++- spec/features/login_spec.rb | 10 +- spec/features/markdown_spec.rb | 4 + .../merge_requests/created_from_fork_spec.rb | 58 +++ .../user_lists_merge_requests_spec.rb | 161 ++++++ spec/features/notes_on_merge_requests_spec.rb | 12 +- spec/features/participants_autocomplete_spec.rb | 6 +- spec/features/pipelines_spec.rb | 189 +++++++ spec/features/project/shortcuts_spec.rb | 21 - spec/features/projects/badges/list_spec.rb | 5 +- ...eloper_views_empty_project_instructions_spec.rb | 63 +++ .../projects/files/gitignore_dropdown_spec.rb | 30 ++ .../project_owner_creates_license_file_spec.rb | 8 +- ...to_create_license_file_in_empty_project_spec.rb | 4 +- spec/features/projects/shortcuts_spec.rb | 21 + spec/features/runners_spec.rb | 37 +- spec/features/signup_spec.rb | 45 +- spec/features/tags/master_creates_tag_spec.rb | 62 +++ spec/features/tags/master_deletes_tag_spec.rb | 41 ++ spec/features/tags/master_updates_tag_spec.rb | 42 ++ spec/features/tags/master_views_tags_spec.rb | 73 +++ spec/features/task_lists_spec.rb | 5 +- spec/features/todos/target_state_spec.rb | 65 +++ spec/features/todos/todos_spec.rb | 21 + spec/features/variables_spec.rb | 61 ++- spec/finders/issues_finder_spec.rb | 180 ++++--- spec/fixtures/container_registry/config_blob.json | 1 + spec/fixtures/container_registry/tag_manifest.json | 1 + spec/fixtures/markdown.md.erb | 23 +- spec/helpers/auth_helper_spec.rb | 47 +- spec/helpers/diff_helper_spec.rb | 4 +- spec/helpers/events_helper_spec.rb | 95 ++-- spec/helpers/merge_requests_helper_spec.rb | 2 +- spec/helpers/projects_helper_spec.rb | 10 +- spec/javascripts/fixtures/right_sidebar.html.haml | 13 + .../graphs/stat_graph_contributors_graph_spec.js | 128 +++++ .../graphs/stat_graph_contributors_util_spec.js | 212 ++++++++ spec/javascripts/graphs/stat_graph_spec.js | 19 + spec/javascripts/project_title_spec.js.coffee | 1 + spec/javascripts/right_sidebar_spec.js.coffee | 69 +++ .../stat_graph_contributors_graph_spec.js | 128 ----- .../stat_graph_contributors_util_spec.js | 212 -------- spec/javascripts/stat_graph_spec.js | 19 - spec/lib/award_emoji_spec.rb | 2 +- .../filter/commit_range_reference_filter_spec.rb | 15 - .../banzai/filter/commit_reference_filter_spec.rb | 15 - spec/lib/banzai/filter/inline_diff_filter_spec.rb | 68 +++ .../banzai/filter/issue_reference_filter_spec.rb | 25 - .../banzai/filter/label_reference_filter_spec.rb | 10 - .../filter/merge_request_reference_filter_spec.rb | 15 - .../filter/milestone_reference_filter_spec.rb | 145 +++++- spec/lib/banzai/filter/redactor_filter_spec.rb | 38 +- .../filter/reference_gatherer_filter_spec.rb | 87 ---- spec/lib/banzai/filter/sanitization_filter_spec.rb | 6 + .../banzai/filter/snippet_reference_filter_spec.rb | 15 - spec/lib/banzai/filter/upload_link_filter_spec.rb | 18 + .../banzai/filter/user_reference_filter_spec.rb | 35 +- spec/lib/banzai/filter/wiki_link_filter_spec.rb | 85 ++++ .../banzai/reference_parser/base_parser_spec.rb | 237 +++++++++ .../banzai/reference_parser/commit_parser_spec.rb | 113 +++++ .../reference_parser/commit_range_parser_spec.rb | 120 +++++ .../reference_parser/external_issue_parser_spec.rb | 62 +++ .../banzai/reference_parser/issue_parser_spec.rb | 79 +++ .../banzai/reference_parser/label_parser_spec.rb | 31 ++ .../reference_parser/merge_request_parser_spec.rb | 30 ++ .../reference_parser/milestone_parser_spec.rb | 31 ++ .../banzai/reference_parser/snippet_parser_spec.rb | 31 ++ .../banzai/reference_parser/user_parser_spec.rb | 189 +++++++ spec/lib/ci/ansi2html_spec.rb | 120 +++-- spec/lib/ci/charts_spec.rb | 7 + spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 10 +- spec/lib/container_registry/blob_spec.rb | 61 +++ spec/lib/container_registry/registry_spec.rb | 28 ++ spec/lib/container_registry/repository_spec.rb | 65 +++ spec/lib/container_registry/tag_spec.rb | 89 ++++ spec/lib/gitlab/akismet_helper_spec.rb | 4 +- spec/lib/gitlab/bitbucket_import/client_spec.rb | 2 +- .../ci/build/artifacts/metadata/entry_spec.rb | 6 +- spec/lib/gitlab/database/migration_helpers_spec.rb | 128 +++++ .../gitlab/email/message/repository_push_spec.rb | 4 +- spec/lib/gitlab/gfm/reference_rewriter_spec.rb | 4 +- spec/lib/gitlab/gfm/uploads_rewriter_spec.rb | 8 +- .../gitlab/github_import/branch_formatter_spec.rb | 71 +++ .../github_import/pull_request_formatter_spec.rb | 46 +- spec/lib/gitlab/gitignore_spec.rb | 40 ++ spec/lib/gitlab/import_url_spec.rb | 21 - spec/lib/gitlab/lazy_spec.rb | 37 ++ spec/lib/gitlab/lfs/lfs_router_spec.rb | 8 +- spec/lib/gitlab/metrics/instrumentation_spec.rb | 14 +- spec/lib/gitlab/metrics/sampler_spec.rb | 2 +- .../metrics/subscribers/active_record_spec.rb | 2 +- .../gitlab/metrics/subscribers/rails_cache_spec.rb | 16 +- .../gitlab/middleware/rails_queue_duration_spec.rb | 31 ++ spec/lib/gitlab/note_data_builder_spec.rb | 71 ++- spec/lib/gitlab/sherlock/collection_spec.rb | 6 +- spec/lib/gitlab/sherlock/query_spec.rb | 2 +- spec/lib/gitlab/sherlock/transaction_spec.rb | 4 +- spec/lib/gitlab/url_sanitizer_spec.rb | 68 +++ spec/lib/json_web_token/rsa_token_spec.rb | 43 ++ spec/lib/json_web_token/token_spec.rb | 18 + spec/mailers/notify_spec.rb | 54 +- spec/mailers/previews/devise_mailer_preview.rb | 11 + spec/mailers/shared/notify.rb | 4 +- spec/models/ability_spec.rb | 117 +++++ spec/models/abuse_report_spec.rb | 12 - spec/models/application_setting_spec.rb | 55 +-- spec/models/broadcast_message_spec.rb | 14 - spec/models/build_spec.rb | 98 ++-- spec/models/ci/commit_spec.rb | 21 +- spec/models/ci/runner_project_spec.rb | 17 - spec/models/ci/runner_spec.rb | 51 +- spec/models/ci/trigger_spec.rb | 13 - spec/models/ci/variable_spec.rb | 16 +- spec/models/commit_range_spec.rb | 34 ++ spec/models/commit_spec.rb | 40 +- spec/models/commit_status_spec.rb | 34 -- spec/models/concerns/issuable_spec.rb | 54 +- spec/models/concerns/participable_spec.rb | 83 ++++ spec/models/concerns/subscribable_spec.rb | 10 + spec/models/concerns/token_authenticatable_spec.rb | 6 +- spec/models/deploy_key_spec.rb | 15 - spec/models/deploy_keys_project_spec.rb | 11 - spec/models/email_spec.rb | 11 - spec/models/event_spec.rb | 16 - spec/models/forked_project_link_spec.rb | 11 - spec/models/generic_commit_status_spec.rb | 38 +- spec/models/group_spec.rb | 15 - spec/models/hooks/service_hook_spec.rb | 4 +- spec/models/hooks/system_hook_spec.rb | 20 +- spec/models/hooks/web_hook_spec.rb | 4 +- spec/models/identity_spec.rb | 12 - spec/models/issue_spec.rb | 77 ++- spec/models/key_spec.rb | 15 - spec/models/label_link_spec.rb | 12 - spec/models/label_spec.rb | 21 +- spec/models/legacy_diff_note_spec.rb | 76 +++ spec/models/member_spec.rb | 19 - spec/models/members/project_member_spec.rb | 42 ++ spec/models/merge_request_spec.rb | 99 ++-- spec/models/milestone_spec.rb | 56 ++- spec/models/namespace_spec.rb | 29 +- spec/models/note_spec.rb | 187 +++---- .../project_services/hipchat_service_spec.rb | 151 +++--- .../slack_service/build_message_spec.rb | 23 +- .../slack_service/issue_message_spec.rb | 11 +- .../slack_service/note_message_spec.rb | 4 +- spec/models/project_services/slack_service_spec.rb | 68 ++- spec/models/project_snippet_spec.rb | 16 - spec/models/project_spec.rb | 132 +++-- spec/models/project_wiki_spec.rb | 16 +- spec/models/protected_branch_spec.rb | 12 - spec/models/release_spec.rb | 12 - spec/models/repository_spec.rb | 46 +- spec/models/service_spec.rb | 21 - spec/models/snippet_spec.rb | 43 +- spec/models/todo_spec.rb | 18 - spec/models/user_spec.rb | 89 +--- spec/requests/api/builds_spec.rb | 4 +- spec/requests/api/commit_status_spec.rb | 210 -------- spec/requests/api/commit_statuses_spec.rb | 210 ++++++++ spec/requests/api/gitignores_spec.rb | 29 ++ spec/requests/api/group_members_spec.rb | 10 +- spec/requests/api/groups_spec.rb | 21 +- spec/requests/api/issues_spec.rb | 33 ++ spec/requests/api/labels_spec.rb | 82 ++++ spec/requests/api/licenses_spec.rb | 12 +- spec/requests/api/merge_requests_spec.rb | 28 ++ spec/requests/api/notes_spec.rb | 71 ++- spec/requests/api/project_members_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 21 +- spec/requests/api/runners_spec.rb | 15 +- spec/requests/api/system_hooks_spec.rb | 2 +- spec/requests/api/users_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 40 +- spec/requests/ci/api/runners_spec.rb | 83 +++- spec/requests/jwt_controller_spec.rb | 72 +++ spec/routing/admin_routing_spec.rb | 7 + spec/routing/routing_spec.rb | 51 +- ...ntainer_registry_authentication_service_spec.rb | 242 +++++++++ spec/services/create_commit_builds_service_spec.rb | 2 +- spec/services/git_push_service_spec.rb | 43 -- spec/services/groups/create_service_spec.rb | 4 +- spec/services/issues/bulk_update_service_spec.rb | 9 +- spec/services/issues/create_service_spec.rb | 6 +- spec/services/issues/move_service_spec.rb | 6 +- spec/services/issues/update_service_spec.rb | 24 +- .../add_todo_when_build_fails_service_spec.rb | 81 +++ .../services/merge_requests/create_service_spec.rb | 4 +- spec/services/merge_requests/merge_service_spec.rb | 15 + .../merge_when_build_succeeds_service_spec.rb | 16 +- .../merge_requests/refresh_service_spec.rb | 28 ++ .../services/merge_requests/update_service_spec.rb | 8 +- spec/services/notification_service_spec.rb | 30 ++ spec/services/projects/create_service_spec.rb | 4 +- spec/services/projects/destroy_service_spec.rb | 31 +- spec/services/projects/fork_service_spec.rb | 27 + spec/services/projects/import_service_spec.rb | 2 +- spec/services/projects/transfer_service_spec.rb | 11 + spec/services/system_note_service_spec.rb | 26 +- spec/services/todo_service_spec.rb | 38 ++ spec/support/filter_spec_helper.rb | 3 +- spec/support/jira_service_helper.rb | 10 +- spec/support/login_helpers.rb | 6 +- spec/support/markdown_feature.rb | 6 +- spec/support/matchers/markdown_matchers.rb | 12 +- spec/support/reference_parser_helpers.rb | 5 + spec/support/stub_gitlab_calls.rb | 37 +- spec/tasks/gitlab/backup_rake_spec.rb | 32 +- spec/tasks/gitlab/db_rake_spec.rb | 62 +++ spec/teaspoon_env.rb | 50 +- spec/workers/emails_on_push_worker_spec.rb | 65 ++- spec/workers/post_receive_spec.rb | 16 + spec/workers/repository_import_worker_spec.rb | 26 +- vendor/assets/javascripts/jquery.scrollTo.js | 210 ++++++++ vendor/assets/stylesheets/animate.css | 11 - vendor/gitignore/Actionscript.gitignore | 19 + vendor/gitignore/Ada.gitignore | 5 + vendor/gitignore/Agda.gitignore | 1 + vendor/gitignore/Android.gitignore | 39 ++ vendor/gitignore/AppEngine.gitignore | 2 + vendor/gitignore/AppceleratorTitanium.gitignore | 3 + vendor/gitignore/ArchLinuxPackages.gitignore | 13 + vendor/gitignore/Autotools.gitignore | 18 + vendor/gitignore/C++.gitignore | 28 ++ vendor/gitignore/C.gitignore | 33 ++ vendor/gitignore/CFWheels.gitignore | 12 + vendor/gitignore/CMake.gitignore | 6 + vendor/gitignore/CUDA.gitignore | 6 + vendor/gitignore/CakePHP.gitignore | 25 + vendor/gitignore/ChefCookbook.gitignore | 9 + vendor/gitignore/Clojure.gitignore | 1 + vendor/gitignore/CodeIgniter.gitignore | 6 + vendor/gitignore/CommonLisp.gitignore | 3 + vendor/gitignore/Composer.gitignore | 6 + vendor/gitignore/Concrete5.gitignore | 4 + vendor/gitignore/Coq.gitignore | 3 + vendor/gitignore/CraftCMS.gitignore | 3 + vendor/gitignore/D.gitignore | 20 + vendor/gitignore/DM.gitignore | 5 + vendor/gitignore/Dart.gitignore | 27 + vendor/gitignore/Delphi.gitignore | 66 +++ vendor/gitignore/Drupal.gitignore | 36 ++ vendor/gitignore/EPiServer.gitignore | 4 + vendor/gitignore/Eagle.gitignore | 44 ++ vendor/gitignore/Elisp.gitignore | 5 + vendor/gitignore/Elixir.gitignore | 5 + vendor/gitignore/Elm.gitignore | 4 + vendor/gitignore/Erlang.gitignore | 10 + vendor/gitignore/ExpressionEngine.gitignore | 19 + vendor/gitignore/ExtJs.gitignore | 4 + vendor/gitignore/Fancy.gitignore | 2 + vendor/gitignore/Finale.gitignore | 13 + vendor/gitignore/ForceDotCom.gitignore | 4 + vendor/gitignore/Fortran.gitignore | 1 + vendor/gitignore/FuelPHP.gitignore | 21 + vendor/gitignore/GWT.gitignore | 28 ++ vendor/gitignore/Gcov.gitignore | 5 + vendor/gitignore/GitBook.gitignore | 16 + vendor/gitignore/Global/Anjuta.gitignore | 3 + vendor/gitignore/Global/Archives.gitignore | 27 + vendor/gitignore/Global/BricxCC.gitignore | 4 + vendor/gitignore/Global/CVS.gitignore | 4 + vendor/gitignore/Global/Calabash.gitignore | 10 + vendor/gitignore/Global/Cloud9.gitignore | 3 + vendor/gitignore/Global/CodeKit.gitignore | 3 + vendor/gitignore/Global/DartEditor.gitignore | 2 + vendor/gitignore/Global/Dreamweaver.gitignore | 7 + vendor/gitignore/Global/Dropbox.gitignore | 4 + vendor/gitignore/Global/Eclipse.gitignore | 51 ++ vendor/gitignore/Global/EiffelStudio.gitignore | 2 + vendor/gitignore/Global/Emacs.gitignore | 42 ++ vendor/gitignore/Global/Ensime.gitignore | 4 + vendor/gitignore/Global/Espresso.gitignore | 1 + vendor/gitignore/Global/FlexBuilder.gitignore | 3 + vendor/gitignore/Global/GPG.gitignore | 2 + vendor/gitignore/Global/IPythonNotebook.gitignore | 2 + vendor/gitignore/Global/JDeveloper.gitignore | 13 + vendor/gitignore/Global/JetBrains.gitignore | 44 ++ vendor/gitignore/Global/KDevelop4.gitignore | 2 + vendor/gitignore/Global/Kate.gitignore | 3 + vendor/gitignore/Global/Lazarus.gitignore | 30 ++ vendor/gitignore/Global/LibreOffice.gitignore | 2 + vendor/gitignore/Global/Linux.gitignore | 10 + vendor/gitignore/Global/LyX.gitignore | 4 + vendor/gitignore/Global/Matlab.gitignore | 19 + vendor/gitignore/Global/Mercurial.gitignore | 6 + vendor/gitignore/Global/MicrosoftOffice.gitignore | 16 + vendor/gitignore/Global/ModelSim.gitignore | 23 + vendor/gitignore/Global/Momentics.gitignore | 8 + vendor/gitignore/Global/MonoDevelop.gitignore | 8 + vendor/gitignore/Global/NetBeans.gitignore | 7 + vendor/gitignore/Global/Ninja.gitignore | 2 + vendor/gitignore/Global/NotepadPP.gitignore | 2 + vendor/gitignore/Global/OSX.gitignore | 24 + vendor/gitignore/Global/Otto.gitignore | 1 + vendor/gitignore/Global/Redcar.gitignore | 1 + vendor/gitignore/Global/Redis.gitignore | 3 + vendor/gitignore/Global/SBT.gitignore | 9 + vendor/gitignore/Global/SVN.gitignore | 1 + vendor/gitignore/Global/SlickEdit.gitignore | 11 + vendor/gitignore/Global/SublimeText.gitignore | 14 + vendor/gitignore/Global/SynopsysVCS.gitignore | 36 ++ vendor/gitignore/Global/Tags.gitignore | 16 + vendor/gitignore/Global/TextMate.gitignore | 3 + vendor/gitignore/Global/TortoiseGit.gitignore | 2 + vendor/gitignore/Global/Vagrant.gitignore | 1 + vendor/gitignore/Global/Vim.gitignore | 10 + vendor/gitignore/Global/VirtualEnv.gitignore | 12 + vendor/gitignore/Global/VisualStudioCode.gitignore | 2 + vendor/gitignore/Global/WebMethods.gitignore | 14 + vendor/gitignore/Global/Windows.gitignore | 18 + vendor/gitignore/Global/Xcode.gitignore | 23 + vendor/gitignore/Global/XilinxISE.gitignore | 67 +++ vendor/gitignore/Go.gitignore | 24 + vendor/gitignore/Gradle.gitignore | 14 + vendor/gitignore/Grails.gitignore | 33 ++ vendor/gitignore/Haskell.gitignore | 18 + vendor/gitignore/IGORPro.gitignore | 5 + vendor/gitignore/Idris.gitignore | 2 + vendor/gitignore/Java.gitignore | 12 + vendor/gitignore/Jboss.gitignore | 19 + vendor/gitignore/Jekyll.gitignore | 3 + vendor/gitignore/Joomla.gitignore | 546 +++++++++++++++++++++ vendor/gitignore/KiCad.gitignore | 20 + vendor/gitignore/Kohana.gitignore | 2 + vendor/gitignore/LabVIEW.gitignore | 16 + vendor/gitignore/Laravel.gitignore | 16 + vendor/gitignore/Leiningen.gitignore | 12 + vendor/gitignore/LemonStand.gitignore | 21 + vendor/gitignore/Lilypond.gitignore | 6 + vendor/gitignore/Lithium.gitignore | 2 + vendor/gitignore/Lua.gitignore | 41 ++ vendor/gitignore/Magento.gitignore | 104 ++++ vendor/gitignore/Maven.gitignore | 9 + vendor/gitignore/Mercury.gitignore | 13 + vendor/gitignore/MetaProgrammingSystem.gitignore | 16 + vendor/gitignore/Nanoc.gitignore | 10 + vendor/gitignore/Nim.gitignore | 1 + vendor/gitignore/Node.gitignore | 37 ++ vendor/gitignore/OCaml.gitignore | 20 + vendor/gitignore/Objective-C.gitignore | 51 ++ vendor/gitignore/Opa.gitignore | 13 + vendor/gitignore/OpenCart.gitignore | 13 + vendor/gitignore/OracleForms.gitignore | 8 + vendor/gitignore/Packer.gitignore | 5 + vendor/gitignore/Perl.gitignore | 20 + vendor/gitignore/Phalcon.gitignore | 2 + vendor/gitignore/PlayFramework.gitignore | 15 + vendor/gitignore/Plone.gitignore | 18 + vendor/gitignore/Prestashop.gitignore | 32 ++ vendor/gitignore/Processing.gitignore | 7 + vendor/gitignore/Python.gitignore | 89 ++++ vendor/gitignore/Qooxdoo.gitignore | 5 + vendor/gitignore/Qt.gitignore | 38 ++ vendor/gitignore/R.gitignore | 33 ++ vendor/gitignore/README.md | 14 + vendor/gitignore/ROS.gitignore | 47 ++ vendor/gitignore/Rails.gitignore | 38 ++ vendor/gitignore/RhodesRhomobile.gitignore | 9 + vendor/gitignore/Ruby.gitignore | 50 ++ vendor/gitignore/Rust.gitignore | 7 + vendor/gitignore/SCons.gitignore | 2 + vendor/gitignore/Sass.gitignore | 2 + vendor/gitignore/Scala.gitignore | 17 + vendor/gitignore/Scheme.gitignore | 7 + vendor/gitignore/Scrivener.gitignore | 7 + vendor/gitignore/Sdcc.gitignore | 8 + vendor/gitignore/SeamGen.gitignore | 26 + vendor/gitignore/SketchUp.gitignore | 1 + vendor/gitignore/Smalltalk.gitignore | 18 + vendor/gitignore/Stella.gitignore | 12 + vendor/gitignore/SugarCRM.gitignore | 25 + vendor/gitignore/Swift.gitignore | 63 +++ vendor/gitignore/Symfony.gitignore | 48 ++ vendor/gitignore/SymphonyCMS.gitignore | 6 + vendor/gitignore/TeX.gitignore | 180 +++++++ vendor/gitignore/Terraform.gitignore | 3 + vendor/gitignore/Textpattern.gitignore | 11 + vendor/gitignore/TurboGears2.gitignore | 20 + vendor/gitignore/Typo3.gitignore | 20 + vendor/gitignore/Umbraco.gitignore | 19 + vendor/gitignore/Unity.gitignore | 30 ++ vendor/gitignore/UnrealEngine.gitignore | 62 +++ vendor/gitignore/VVVV.gitignore | 6 + vendor/gitignore/VisualStudio.gitignore | 252 ++++++++++ vendor/gitignore/Waf.gitignore | 4 + vendor/gitignore/WordPress.gitignore | 18 + vendor/gitignore/Xojo.gitignore | 11 + vendor/gitignore/Yeoman.gitignore | 6 + vendor/gitignore/Yii.gitignore | 6 + vendor/gitignore/ZendFramework.gitignore | 25 + vendor/gitignore/Zephir.gitignore | 26 + 1210 files changed, 21355 insertions(+), 9327 deletions(-) create mode 100644 .vagrant_enabled delete mode 100644 app/assets/images/ci/arch.jpg delete mode 100644 app/assets/images/ci/favicon.ico delete mode 100644 app/assets/images/ci/loader.gif delete mode 100644 app/assets/images/ci/no_avatar.png delete mode 100644 app/assets/images/ci/rails.png delete mode 100644 app/assets/images/ci/service_sample.png create mode 100644 app/assets/images/mailers/gitlab_header_logo.png create mode 100644 app/assets/images/mailers/gitlab_tanuki_2x.png create mode 100644 app/assets/javascripts/blob/blob_gitignore_selector.js.coffee delete mode 100644 app/assets/javascripts/calendar.js.coffee create mode 100644 app/assets/javascripts/graphs/application.js.coffee create mode 100644 app/assets/javascripts/graphs/stat_graph.js.coffee create mode 100644 app/assets/javascripts/graphs/stat_graph_contributors.js.coffee create mode 100644 app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee create mode 100644 app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee delete mode 100644 app/assets/javascripts/issues.js.coffee create mode 100644 app/assets/javascripts/layout_nav.js.coffee create mode 100644 app/assets/javascripts/lib/type_utility.js.coffee delete mode 100644 app/assets/javascripts/stat_graph.js.coffee delete mode 100644 app/assets/javascripts/stat_graph_contributors.js.coffee delete mode 100644 app/assets/javascripts/stat_graph_contributors_graph.js.coffee delete mode 100644 app/assets/javascripts/stat_graph_contributors_util.js.coffee create mode 100644 app/assets/javascripts/users/application.js.coffee create mode 100644 app/assets/javascripts/users/calendar.js.coffee create mode 100644 app/assets/stylesheets/framework/animations.scss create mode 100644 app/assets/stylesheets/mailers/devise.scss create mode 100644 app/assets/stylesheets/mailers/repository_push_email.scss create mode 100644 app/assets/stylesheets/pages/pipelines.scss create mode 100644 app/controllers/admin/health_check_controller.rb create mode 100644 app/controllers/health_check_controller.rb create mode 100644 app/controllers/jwt_controller.rb create mode 100644 app/controllers/projects/container_registry_controller.rb create mode 100644 app/controllers/projects/pipelines_controller.rb create mode 100644 app/finders/pipelines_finder.rb create mode 100644 app/helpers/javascript_helper.rb create mode 100644 app/models/legacy_diff_note.rb create mode 100644 app/services/auth/container_registry_authentication_service.rb create mode 100644 app/services/ci/create_pipeline_service.rb create mode 100644 app/services/merge_requests/add_todo_when_build_fails_service.rb create mode 100644 app/views/admin/health_check/show.html.haml delete mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.haml create mode 100644 app/views/devise/mailer/confirmation_instructions.text.erb create mode 100644 app/views/events/_event.atom.builder create mode 100644 app/views/issues/_issue.atom.builder create mode 100644 app/views/layouts/devise_mailer.html.haml delete mode 100644 app/views/projects/blob/_header_title.html.haml delete mode 100644 app/views/projects/branches/destroy.js.haml delete mode 100644 app/views/projects/builds/_header_title.html.haml create mode 100644 app/views/projects/ci/commits/_commit.html.haml create mode 100644 app/views/projects/commit/_ci_stage.html.haml create mode 100644 app/views/projects/commits/_commit.atom.builder delete mode 100644 app/views/projects/commits/_header_title.html.haml create mode 100644 app/views/projects/container_registry/_tag.html.haml create mode 100644 app/views/projects/container_registry/index.html.haml delete mode 100644 app/views/projects/graphs/_header_title.html.haml delete mode 100644 app/views/projects/issues/_header_title.html.haml delete mode 100644 app/views/projects/labels/_header_title.html.haml delete mode 100644 app/views/projects/merge_requests/_header_title.html.haml delete mode 100644 app/views/projects/milestones/_header_title.html.haml delete mode 100644 app/views/projects/notes/_commit_discussion.html.haml delete mode 100644 app/views/projects/notes/discussions/_active.html.haml delete mode 100644 app/views/projects/notes/discussions/_commit.html.haml delete mode 100644 app/views/projects/notes/discussions/_diff.html.haml create mode 100644 app/views/projects/notes/discussions/_diff_with_notes.html.haml create mode 100644 app/views/projects/notes/discussions/_notes.html.haml delete mode 100644 app/views/projects/notes/discussions/_outdated.html.haml create mode 100644 app/views/projects/pipelines/_head.html.haml create mode 100644 app/views/projects/pipelines/_info.html.haml create mode 100644 app/views/projects/pipelines/index.html.haml create mode 100644 app/views/projects/pipelines/new.html.haml create mode 100644 app/views/projects/pipelines/show.html.haml delete mode 100644 app/views/projects/project_members/_header_title.html.haml create mode 100644 app/views/projects/runners/_form.html.haml delete mode 100644 app/views/projects/snippets/_header_title.html.haml create mode 100644 app/views/projects/variables/_content.html.haml create mode 100644 app/views/projects/variables/_form.html.haml create mode 100644 app/views/projects/variables/_table.html.haml create mode 100644 app/views/projects/variables/index.html.haml delete mode 100644 app/views/projects/wikis/_header_title.html.haml delete mode 100644 config/initializers/devise_async.rb create mode 100644 config/initializers/health_check.rb delete mode 100644 config/initializers/monkey_patch.rb create mode 100644 db/migrate/20160407120251_add_images_enabled_for_project.rb create mode 100644 db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb create mode 100644 db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb create mode 100644 db/migrate/20160508215820_add_type_to_notes.rb create mode 100644 db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb create mode 100644 db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb create mode 100644 db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb create mode 100644 db/migrate/20160525205328_remove_main_language_from_projects.rb create mode 100644 db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb create mode 100644 db/migrate/20160528043124_add_users_state_index.rb create mode 100644 db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb create mode 100644 doc/administration/container_registry.md create mode 100644 doc/administration/img/high_availability/active-active-diagram.png create mode 100644 doc/administration/img/high_availability/active-passive-diagram.png delete mode 100644 doc/ci/deployment/README.md create mode 100644 doc/ci/examples/deployment/README.md create mode 100644 doc/container_registry/README.md create mode 100644 doc/container_registry/img/container_registry.png create mode 100644 doc/container_registry/img/project_feature.png create mode 100644 doc/integration/img/enabled-oauth-sign-in-sources.png create mode 100644 doc/monitoring/health_check.md create mode 100644 doc/monitoring/img/health_check_token.png create mode 100644 doc/security/user_email_confirmation.md create mode 100644 doc/update/8.7-to-8.8.md delete mode 100644 features/project/commits/tags.feature delete mode 100644 features/steps/project/commits/tags.rb create mode 100644 features/steps/shared/sidebar_active_tab.rb create mode 100644 generator_templates/active_record/migration/create_table_migration.rb create mode 100644 generator_templates/active_record/migration/migration.rb create mode 100644 lib/api/gitignores.rb create mode 100644 lib/api/subscriptions.rb create mode 100644 lib/backup/registry.rb create mode 100644 lib/banzai/filter/inline_diff_filter.rb delete mode 100644 lib/banzai/filter/reference_gatherer_filter.rb delete mode 100644 lib/banzai/lazy_reference.rb delete mode 100644 lib/banzai/pipeline/reference_extraction_pipeline.rb create mode 100644 lib/banzai/reference_parser.rb create mode 100644 lib/banzai/reference_parser/base_parser.rb create mode 100644 lib/banzai/reference_parser/commit_parser.rb create mode 100644 lib/banzai/reference_parser/commit_range_parser.rb create mode 100644 lib/banzai/reference_parser/external_issue_parser.rb create mode 100644 lib/banzai/reference_parser/issue_parser.rb create mode 100644 lib/banzai/reference_parser/label_parser.rb create mode 100644 lib/banzai/reference_parser/merge_request_parser.rb create mode 100644 lib/banzai/reference_parser/milestone_parser.rb create mode 100644 lib/banzai/reference_parser/snippet_parser.rb create mode 100644 lib/banzai/reference_parser/user_parser.rb create mode 100644 lib/container_registry/blob.rb create mode 100644 lib/container_registry/client.rb create mode 100644 lib/container_registry/config.rb create mode 100644 lib/container_registry/registry.rb create mode 100644 lib/container_registry/repository.rb create mode 100644 lib/container_registry/tag.rb create mode 100644 lib/gitlab/database/migration_helpers.rb create mode 100644 lib/gitlab/github_import/branch_formatter.rb create mode 100644 lib/gitlab/gitignore.rb delete mode 100644 lib/gitlab/import_url.rb create mode 100644 lib/gitlab/lazy.rb create mode 100644 lib/gitlab/middleware/rails_queue_duration.rb create mode 100644 lib/gitlab/url_sanitizer.rb create mode 100644 lib/json_web_token/rsa_token.rb create mode 100644 lib/json_web_token/token.rb create mode 100644 lib/support/nginx/registry-ssl delete mode 100644 lib/tasks/auto_annotate_models.rake create mode 100644 lib/tasks/gitlab/update_gitignore.rake create mode 100644 shared/registry/.gitkeep create mode 100644 spec/controllers/health_check_controller_spec.rb create mode 100644 spec/controllers/registrations_controller_spec.rb create mode 100644 spec/features/admin/admin_health_check_spec.rb create mode 100644 spec/features/container_registry_spec.rb create mode 100644 spec/features/merge_requests/created_from_fork_spec.rb create mode 100644 spec/features/merge_requests/user_lists_merge_requests_spec.rb create mode 100644 spec/features/pipelines_spec.rb delete mode 100644 spec/features/project/shortcuts_spec.rb create mode 100644 spec/features/projects/developer_views_empty_project_instructions_spec.rb create mode 100644 spec/features/projects/files/gitignore_dropdown_spec.rb create mode 100644 spec/features/projects/shortcuts_spec.rb create mode 100644 spec/features/tags/master_creates_tag_spec.rb create mode 100644 spec/features/tags/master_deletes_tag_spec.rb create mode 100644 spec/features/tags/master_updates_tag_spec.rb create mode 100644 spec/features/tags/master_views_tags_spec.rb create mode 100644 spec/features/todos/target_state_spec.rb create mode 100644 spec/fixtures/container_registry/config_blob.json create mode 100644 spec/fixtures/container_registry/tag_manifest.json create mode 100644 spec/javascripts/fixtures/right_sidebar.html.haml create mode 100644 spec/javascripts/graphs/stat_graph_contributors_graph_spec.js create mode 100644 spec/javascripts/graphs/stat_graph_contributors_util_spec.js create mode 100644 spec/javascripts/graphs/stat_graph_spec.js create mode 100644 spec/javascripts/right_sidebar_spec.js.coffee delete mode 100644 spec/javascripts/stat_graph_contributors_graph_spec.js delete mode 100644 spec/javascripts/stat_graph_contributors_util_spec.js delete mode 100644 spec/javascripts/stat_graph_spec.js create mode 100644 spec/lib/banzai/filter/inline_diff_filter_spec.rb delete mode 100644 spec/lib/banzai/filter/reference_gatherer_filter_spec.rb create mode 100644 spec/lib/banzai/filter/wiki_link_filter_spec.rb create mode 100644 spec/lib/banzai/reference_parser/base_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/commit_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/commit_range_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/external_issue_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/issue_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/label_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/merge_request_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/milestone_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/snippet_parser_spec.rb create mode 100644 spec/lib/banzai/reference_parser/user_parser_spec.rb create mode 100644 spec/lib/container_registry/blob_spec.rb create mode 100644 spec/lib/container_registry/registry_spec.rb create mode 100644 spec/lib/container_registry/repository_spec.rb create mode 100644 spec/lib/container_registry/tag_spec.rb create mode 100644 spec/lib/gitlab/database/migration_helpers_spec.rb create mode 100644 spec/lib/gitlab/github_import/branch_formatter_spec.rb create mode 100644 spec/lib/gitlab/gitignore_spec.rb delete mode 100644 spec/lib/gitlab/import_url_spec.rb create mode 100644 spec/lib/gitlab/lazy_spec.rb create mode 100644 spec/lib/gitlab/middleware/rails_queue_duration_spec.rb create mode 100644 spec/lib/gitlab/url_sanitizer_spec.rb create mode 100644 spec/lib/json_web_token/rsa_token_spec.rb create mode 100644 spec/lib/json_web_token/token_spec.rb create mode 100644 spec/mailers/previews/devise_mailer_preview.rb create mode 100644 spec/models/ability_spec.rb delete mode 100644 spec/models/ci/runner_project_spec.rb create mode 100644 spec/models/concerns/participable_spec.rb create mode 100644 spec/models/legacy_diff_note_spec.rb delete mode 100644 spec/requests/api/commit_status_spec.rb create mode 100644 spec/requests/api/commit_statuses_spec.rb create mode 100644 spec/requests/api/gitignores_spec.rb create mode 100644 spec/requests/jwt_controller_spec.rb create mode 100644 spec/services/auth/container_registry_authentication_service_spec.rb create mode 100644 spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb create mode 100644 spec/support/reference_parser_helpers.rb create mode 100644 spec/tasks/gitlab/db_rake_spec.rb create mode 100755 vendor/assets/javascripts/jquery.scrollTo.js delete mode 100644 vendor/assets/stylesheets/animate.css create mode 100644 vendor/gitignore/Actionscript.gitignore create mode 100644 vendor/gitignore/Ada.gitignore create mode 100644 vendor/gitignore/Agda.gitignore create mode 100644 vendor/gitignore/Android.gitignore create mode 100644 vendor/gitignore/AppEngine.gitignore create mode 100644 vendor/gitignore/AppceleratorTitanium.gitignore create mode 100644 vendor/gitignore/ArchLinuxPackages.gitignore create mode 100644 vendor/gitignore/Autotools.gitignore create mode 100644 vendor/gitignore/C++.gitignore create mode 100644 vendor/gitignore/C.gitignore create mode 100644 vendor/gitignore/CFWheels.gitignore create mode 100644 vendor/gitignore/CMake.gitignore create mode 100644 vendor/gitignore/CUDA.gitignore create mode 100644 vendor/gitignore/CakePHP.gitignore create mode 100644 vendor/gitignore/ChefCookbook.gitignore create mode 120000 vendor/gitignore/Clojure.gitignore create mode 100644 vendor/gitignore/CodeIgniter.gitignore create mode 100644 vendor/gitignore/CommonLisp.gitignore create mode 100644 vendor/gitignore/Composer.gitignore create mode 100644 vendor/gitignore/Concrete5.gitignore create mode 100644 vendor/gitignore/Coq.gitignore create mode 100644 vendor/gitignore/CraftCMS.gitignore create mode 100644 vendor/gitignore/D.gitignore create mode 100644 vendor/gitignore/DM.gitignore create mode 100644 vendor/gitignore/Dart.gitignore create mode 100644 vendor/gitignore/Delphi.gitignore create mode 100644 vendor/gitignore/Drupal.gitignore create mode 100644 vendor/gitignore/EPiServer.gitignore create mode 100644 vendor/gitignore/Eagle.gitignore create mode 100644 vendor/gitignore/Elisp.gitignore create mode 100644 vendor/gitignore/Elixir.gitignore create mode 100644 vendor/gitignore/Elm.gitignore create mode 100644 vendor/gitignore/Erlang.gitignore create mode 100644 vendor/gitignore/ExpressionEngine.gitignore create mode 100644 vendor/gitignore/ExtJs.gitignore create mode 100644 vendor/gitignore/Fancy.gitignore create mode 100644 vendor/gitignore/Finale.gitignore create mode 100644 vendor/gitignore/ForceDotCom.gitignore create mode 120000 vendor/gitignore/Fortran.gitignore create mode 100644 vendor/gitignore/FuelPHP.gitignore create mode 100644 vendor/gitignore/GWT.gitignore create mode 100644 vendor/gitignore/Gcov.gitignore create mode 100644 vendor/gitignore/GitBook.gitignore create mode 100644 vendor/gitignore/Global/Anjuta.gitignore create mode 100644 vendor/gitignore/Global/Archives.gitignore create mode 100644 vendor/gitignore/Global/BricxCC.gitignore create mode 100644 vendor/gitignore/Global/CVS.gitignore create mode 100644 vendor/gitignore/Global/Calabash.gitignore create mode 100644 vendor/gitignore/Global/Cloud9.gitignore create mode 100644 vendor/gitignore/Global/CodeKit.gitignore create mode 100644 vendor/gitignore/Global/DartEditor.gitignore create mode 100644 vendor/gitignore/Global/Dreamweaver.gitignore create mode 100644 vendor/gitignore/Global/Dropbox.gitignore create mode 100644 vendor/gitignore/Global/Eclipse.gitignore create mode 100644 vendor/gitignore/Global/EiffelStudio.gitignore create mode 100644 vendor/gitignore/Global/Emacs.gitignore create mode 100644 vendor/gitignore/Global/Ensime.gitignore create mode 100644 vendor/gitignore/Global/Espresso.gitignore create mode 100644 vendor/gitignore/Global/FlexBuilder.gitignore create mode 100644 vendor/gitignore/Global/GPG.gitignore create mode 100644 vendor/gitignore/Global/IPythonNotebook.gitignore create mode 100644 vendor/gitignore/Global/JDeveloper.gitignore create mode 100644 vendor/gitignore/Global/JetBrains.gitignore create mode 100644 vendor/gitignore/Global/KDevelop4.gitignore create mode 100644 vendor/gitignore/Global/Kate.gitignore create mode 100644 vendor/gitignore/Global/Lazarus.gitignore create mode 100644 vendor/gitignore/Global/LibreOffice.gitignore create mode 100644 vendor/gitignore/Global/Linux.gitignore create mode 100644 vendor/gitignore/Global/LyX.gitignore create mode 100644 vendor/gitignore/Global/Matlab.gitignore create mode 100644 vendor/gitignore/Global/Mercurial.gitignore create mode 100644 vendor/gitignore/Global/MicrosoftOffice.gitignore create mode 100644 vendor/gitignore/Global/ModelSim.gitignore create mode 100644 vendor/gitignore/Global/Momentics.gitignore create mode 100644 vendor/gitignore/Global/MonoDevelop.gitignore create mode 100644 vendor/gitignore/Global/NetBeans.gitignore create mode 100644 vendor/gitignore/Global/Ninja.gitignore create mode 100644 vendor/gitignore/Global/NotepadPP.gitignore create mode 100644 vendor/gitignore/Global/OSX.gitignore create mode 100644 vendor/gitignore/Global/Otto.gitignore create mode 100644 vendor/gitignore/Global/Redcar.gitignore create mode 100644 vendor/gitignore/Global/Redis.gitignore create mode 100644 vendor/gitignore/Global/SBT.gitignore create mode 100644 vendor/gitignore/Global/SVN.gitignore create mode 100644 vendor/gitignore/Global/SlickEdit.gitignore create mode 100644 vendor/gitignore/Global/SublimeText.gitignore create mode 100644 vendor/gitignore/Global/SynopsysVCS.gitignore create mode 100644 vendor/gitignore/Global/Tags.gitignore create mode 100644 vendor/gitignore/Global/TextMate.gitignore create mode 100644 vendor/gitignore/Global/TortoiseGit.gitignore create mode 100644 vendor/gitignore/Global/Vagrant.gitignore create mode 100644 vendor/gitignore/Global/Vim.gitignore create mode 100644 vendor/gitignore/Global/VirtualEnv.gitignore create mode 100644 vendor/gitignore/Global/VisualStudioCode.gitignore create mode 100644 vendor/gitignore/Global/WebMethods.gitignore create mode 100644 vendor/gitignore/Global/Windows.gitignore create mode 100644 vendor/gitignore/Global/Xcode.gitignore create mode 100644 vendor/gitignore/Global/XilinxISE.gitignore create mode 100644 vendor/gitignore/Go.gitignore create mode 100644 vendor/gitignore/Gradle.gitignore create mode 100644 vendor/gitignore/Grails.gitignore create mode 100644 vendor/gitignore/Haskell.gitignore create mode 100644 vendor/gitignore/IGORPro.gitignore create mode 100644 vendor/gitignore/Idris.gitignore create mode 100644 vendor/gitignore/Java.gitignore create mode 100644 vendor/gitignore/Jboss.gitignore create mode 100644 vendor/gitignore/Jekyll.gitignore create mode 100644 vendor/gitignore/Joomla.gitignore create mode 100644 vendor/gitignore/KiCad.gitignore create mode 100644 vendor/gitignore/Kohana.gitignore create mode 100644 vendor/gitignore/LabVIEW.gitignore create mode 100644 vendor/gitignore/Laravel.gitignore create mode 100644 vendor/gitignore/Leiningen.gitignore create mode 100644 vendor/gitignore/LemonStand.gitignore create mode 100644 vendor/gitignore/Lilypond.gitignore create mode 100644 vendor/gitignore/Lithium.gitignore create mode 100644 vendor/gitignore/Lua.gitignore create mode 100644 vendor/gitignore/Magento.gitignore create mode 100644 vendor/gitignore/Maven.gitignore create mode 100644 vendor/gitignore/Mercury.gitignore create mode 100644 vendor/gitignore/MetaProgrammingSystem.gitignore create mode 100644 vendor/gitignore/Nanoc.gitignore create mode 100644 vendor/gitignore/Nim.gitignore create mode 100644 vendor/gitignore/Node.gitignore create mode 100644 vendor/gitignore/OCaml.gitignore create mode 100644 vendor/gitignore/Objective-C.gitignore create mode 100644 vendor/gitignore/Opa.gitignore create mode 100644 vendor/gitignore/OpenCart.gitignore create mode 100644 vendor/gitignore/OracleForms.gitignore create mode 100644 vendor/gitignore/Packer.gitignore create mode 100644 vendor/gitignore/Perl.gitignore create mode 100644 vendor/gitignore/Phalcon.gitignore create mode 100644 vendor/gitignore/PlayFramework.gitignore create mode 100644 vendor/gitignore/Plone.gitignore create mode 100644 vendor/gitignore/Prestashop.gitignore create mode 100644 vendor/gitignore/Processing.gitignore create mode 100644 vendor/gitignore/Python.gitignore create mode 100644 vendor/gitignore/Qooxdoo.gitignore create mode 100644 vendor/gitignore/Qt.gitignore create mode 100644 vendor/gitignore/R.gitignore create mode 100644 vendor/gitignore/README.md create mode 100644 vendor/gitignore/ROS.gitignore create mode 100644 vendor/gitignore/Rails.gitignore create mode 100644 vendor/gitignore/RhodesRhomobile.gitignore create mode 100644 vendor/gitignore/Ruby.gitignore create mode 100644 vendor/gitignore/Rust.gitignore create mode 100644 vendor/gitignore/SCons.gitignore create mode 100644 vendor/gitignore/Sass.gitignore create mode 100644 vendor/gitignore/Scala.gitignore create mode 100644 vendor/gitignore/Scheme.gitignore create mode 100644 vendor/gitignore/Scrivener.gitignore create mode 100644 vendor/gitignore/Sdcc.gitignore create mode 100644 vendor/gitignore/SeamGen.gitignore create mode 100644 vendor/gitignore/SketchUp.gitignore create mode 100644 vendor/gitignore/Smalltalk.gitignore create mode 100644 vendor/gitignore/Stella.gitignore create mode 100644 vendor/gitignore/SugarCRM.gitignore create mode 100644 vendor/gitignore/Swift.gitignore create mode 100644 vendor/gitignore/Symfony.gitignore create mode 100644 vendor/gitignore/SymphonyCMS.gitignore create mode 100644 vendor/gitignore/TeX.gitignore create mode 100644 vendor/gitignore/Terraform.gitignore create mode 100644 vendor/gitignore/Textpattern.gitignore create mode 100644 vendor/gitignore/TurboGears2.gitignore create mode 100644 vendor/gitignore/Typo3.gitignore create mode 100644 vendor/gitignore/Umbraco.gitignore create mode 100644 vendor/gitignore/Unity.gitignore create mode 100644 vendor/gitignore/UnrealEngine.gitignore create mode 100644 vendor/gitignore/VVVV.gitignore create mode 100644 vendor/gitignore/VisualStudio.gitignore create mode 100644 vendor/gitignore/Waf.gitignore create mode 100644 vendor/gitignore/WordPress.gitignore create mode 100644 vendor/gitignore/Xojo.gitignore create mode 100644 vendor/gitignore/Yeoman.gitignore create mode 100644 vendor/gitignore/Yii.gitignore create mode 100644 vendor/gitignore/ZendFramework.gitignore create mode 100644 vendor/gitignore/Zephir.gitignore diff --git a/.gitignore b/.gitignore index 8f861d76a37..ce6a363fe35 100644 --- a/.gitignore +++ b/.gitignore @@ -4,46 +4,46 @@ .bundle .chef .directory -.envrc -.gitlab_shell_secret +/.envrc +/.gitlab_shell_secret .idea -.rbenv-version +/.rbenv-version .rbx/ -.ruby-gemset -.ruby-version -.rvmrc +/.ruby-gemset +/.ruby-version +/.rvmrc .sass-cache/ -.secret -.vagrant -.byebug_history -Vagrantfile -backups/* -config/aws.yml -config/database.yml -config/gitlab.yml -config/gitlab_ci.yml -config/initializers/rack_attack.rb -config/initializers/smtp_settings.rb -config/initializers/relative_url.rb -config/resque.yml -config/unicorn.rb -config/secrets.yml -config/sidekiq.yml -coverage/* -db/*.sqlite3 -db/*.sqlite3-journal -db/data.yml -doc/code/* -dump.rdb -log/*.log* -nohup.out -public/assets/ -public/uploads.* -public/uploads/ -shared/artifacts/ -rails_best_practices_output.html +/.secret +/.vagrant +/.byebug_history +/Vagrantfile +/backups/* +/config/aws.yml +/config/database.yml +/config/gitlab.yml +/config/gitlab_ci.yml +/config/initializers/rack_attack.rb +/config/initializers/smtp_settings.rb +/config/initializers/relative_url.rb +/config/resque.yml +/config/unicorn.rb +/config/secrets.yml +/config/sidekiq.yml +/coverage/* +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/data.yml +/doc/code/* +/dump.rdb +/log/*.log* +/nohup.out +/public/assets/ +/public/uploads.* +/public/uploads/ +/shared/artifacts/ +/rails_best_practices_output.html /tags -tmp/ -vendor/bundle/* -builds/* -shared/* +/tmp/* +/vendor/bundle/* +/builds/* +/shared/* diff --git a/.rubocop.yml b/.rubocop.yml index 9f179efa3ce..84a8015b410 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +require: rubocop-rspec + AllCops: TargetRubyVersion: 2.1 # Cop names are not displayed in offense messages by default. Change behavior @@ -21,6 +23,7 @@ AllCops: - 'lib/email_validator.rb' - 'lib/gitlab/upgrader.rb' - 'lib/gitlab/seeder.rb' + - 'generator_templates/**/*' ##################### Style ################################## @@ -56,7 +59,7 @@ Style/AndOr: # Use `Array#join` instead of `Array#*`. Style/ArrayJoin: - Enabled: false + Enabled: true # Use only ascii symbols in comments. Style/AsciiComments: @@ -68,7 +71,7 @@ Style/AsciiIdentifiers: # Checks for uses of Module#attr. Style/Attr: - Enabled: false + Enabled: true # Avoid the use of BEGIN blocks. Style/BeginBlock: @@ -80,7 +83,7 @@ Style/BarePercentLiterals: # Do not use block comments. Style/BlockComments: - Enabled: false + Enabled: true # Put end statement of multiline block on its own line. Style/BlockEndNewline: @@ -121,7 +124,7 @@ Style/ClassCheck: # Use self when defining module/class methods. Style/ClassMethods: - Enabled: false + Enabled: true # Avoid the use of class variables. Style/ClassVars: @@ -151,7 +154,7 @@ Style/ConstantName: # Use def with parentheses when there are arguments. Style/DefWithParentheses: - Enabled: false + Enabled: true # Checks for use of deprecated Hash methods. Style/DeprecatedHashMethods: @@ -215,15 +218,15 @@ Style/EmptyLiteral: # Avoid the use of END blocks. Style/EndBlock: - Enabled: false + Enabled: true # Use Unix-style line endings. Style/EndOfLine: - Enabled: false + Enabled: true # Favor the use of Fixnum#even? && Fixnum#odd? Style/EvenOdd: - Enabled: false + Enabled: true # Do not use unnecessary spacing. Style/ExtraSpacing: @@ -231,15 +234,20 @@ Style/ExtraSpacing: # Use snake_case for source file names. Style/FileName: - Enabled: false + Enabled: true + +# Checks for a line break before the first parameter in a multi-line method +# parameter definition. +Style/FirstMethodParameterLineBreak: + Enabled: true # Checks for flip flops. Style/FlipFlop: - Enabled: false + Enabled: true # Checks use of for or each in multiline loops. Style/For: - Enabled: false + Enabled: true # Enforce the use of Kernel#sprintf, Kernel#format or String#%. Style/FormatString: @@ -247,7 +255,7 @@ Style/FormatString: # Do not introduce global variables. Style/GlobalVars: - Enabled: false + Enabled: true # Check for conditionals that can be replaced with guard clauses. Style/GuardClause: @@ -268,7 +276,7 @@ Style/IfUnlessModifier: # Do not use if x; .... Use the ternary operator instead. Style/IfWithSemicolon: - Enabled: false + Enabled: true # Checks that conditional statements do not have an identical line at the # end of each branch, which can validly be moved out of the conditional. @@ -276,9 +284,9 @@ Style/IdenticalConditionalBranches: Enabled: false # Checks the indentation of the first line of the right-hand-side of a -# multi-line assignment. +# multi-line assignment. Style/IndentAssignment: - Enabled: false + Enabled: true # Keep indentation straight. Style/IndentationConsistency: @@ -298,7 +306,7 @@ Style/IndentHash: # Use Kernel#loop for infinite loops. Style/InfiniteLoop: - Enabled: false + Enabled: true # Use the new lambda literal syntax for single-line blocks. Style/Lambda: @@ -306,11 +314,11 @@ Style/Lambda: # Use lambda.call(...) instead of lambda.(...). Style/LambdaCall: - Enabled: false + Enabled: true # Comments should start with a space. Style/LeadingCommentSpace: - Enabled: false + Enabled: true # Use \ instead of + or << to concatenate two string literals at line end. Style/LineEndConcatenation: @@ -322,16 +330,22 @@ Style/MethodCallParentheses: # Checks if the method definitions have or don't have parentheses. Style/MethodDefParentheses: - Enabled: false + Enabled: true # Use the configured style when naming methods. Style/MethodName: - Enabled: false + Enabled: true # Checks for usage of `extend self` in modules. Style/ModuleFunction: Enabled: false +# Checks that the closing brace in an array literal is either on the same line +# as the last array element, or a new line. +Style/MultilineArrayBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Avoid multi-line chains of blocks. Style/MultilineBlockChain: Enabled: false @@ -340,15 +354,32 @@ Style/MultilineBlockChain: Style/MultilineBlockLayout: Enabled: true +# Checks that the closing brace in a hash literal is either on the same line as +# the last hash element, or a new line. +Style/MultilineHashBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Do not use then for multi-line if/unless. Style/MultilineIfThen: + Enabled: true + +# Checks that the closing brace in a method call is either on the same line as +# the last method argument, or a new line. +Style/MultilineMethodCallBraceLayout: Enabled: false + EnforcedStyle: symmetrical # Checks indentation of method calls with the dot operator that span more than # one line. Style/MultilineMethodCallIndentation: Enabled: false +# Checks that the closing brace in a method definition is symmetrical with +# respect to the opening brace and the method parameters. +Style/MultilineMethodDefinitionBraceLayout: + Enabled: false + # Checks indentation of binary operations that span more than one line. Style/MultilineOperationIndentation: Enabled: false @@ -363,7 +394,7 @@ Style/MutableConstant: # Favor unless over if for negative conditions (or control flow or). Style/NegatedIf: - Enabled: false + Enabled: true # Favor until over while for negative conditions. Style/NegatedWhile: @@ -371,7 +402,7 @@ Style/NegatedWhile: # Avoid using nested modifiers. Style/NestedModifier: - Enabled: false + Enabled: true # Parenthesize method calls which are nested inside the argument list of # another parenthesized method call. @@ -408,7 +439,7 @@ Style/OneLineConditional: # When defining binary operators, name the argument other. Style/OpMethod: - Enabled: false + Enabled: true # Check for simple usages of parallel assignment. It will only warn when # the number of variables matches on both sides of the assignment. @@ -455,10 +486,9 @@ Style/RedundantException: Style/RedundantFreeze: Enabled: false -# TODO: Enable RedundantParentheses Cop. # Checks for parentheses that seem not to serve any purpose. Style/RedundantParentheses: - Enabled: false + Enabled: true # Don't use return where it's not required. Style/RedundantReturn: @@ -484,11 +514,12 @@ Style/SelfAssignment: # Don't use semicolons to terminate expressions. Style/Semicolon: - Enabled: false + Enabled: true # Checks for proper usage of fail and raise. Style/SignalException: - Enabled: false + EnforcedStyle: only_raise + Enabled: true # Enforces the names of some block params. Style/SingleLineBlockParams: @@ -509,29 +540,28 @@ Style/SpaceAfterComma: # Do not put a space between a method name and the opening parenthesis in a # method definition. Style/SpaceAfterMethodName: - Enabled: false + Enabled: true # Tracks redundant space after the ! operator. Style/SpaceAfterNot: - Enabled: false + Enabled: true # Use spaces after semicolons. Style/SpaceAfterSemicolon: - Enabled: false + Enabled: true # Checks that the equals signs in parameter default assignments have or don't # have surrounding space depending on configuration. Style/SpaceAroundEqualsInParameterDefault: Enabled: false -# TODO: Enable SpaceAroundKeyword Cop. # Use a space around keywords if appropriate. Style/SpaceAroundKeyword: - Enabled: false + Enabled: true # Use a single space around operators. Style/SpaceAroundOperators: - Enabled: false + Enabled: true # Checks that the left block brace has or doesn't have space before it. Style/SpaceBeforeBlockBraces: @@ -539,11 +569,11 @@ Style/SpaceBeforeBlockBraces: # No spaces before commas. Style/SpaceBeforeComma: - Enabled: false + Enabled: true # Checks for missing space between code and a comment on the same line. Style/SpaceBeforeComment: - Enabled: false + Enabled: true # Checks that exactly one space is used between a method name and the first # argument for method calls without parentheses. @@ -552,7 +582,7 @@ Style/SpaceBeforeFirstArg: # No spaces before semicolons. Style/SpaceBeforeSemicolon: - Enabled: false + Enabled: true # Checks that block braces have or don't have surrounding space. # For blocks taking parameters, checks that the left brace has or doesn't @@ -574,11 +604,12 @@ Style/SpaceInsideParens: # No spaces inside range literals. Style/SpaceInsideRangeLiteral: - Enabled: false + Enabled: true # Checks for padding/surrounding spaces inside string interpolation. Style/SpaceInsideStringInterpolation: - Enabled: false + EnforcedStyle: no_space + Enabled: true # Avoid Perl-style global variables. Style/SpecialGlobalVars: @@ -586,7 +617,8 @@ Style/SpecialGlobalVars: # Check for the usage of parentheses around stabby lambda arguments. Style/StabbyLambdaParentheses: - Enabled: false + EnforcedStyle: require_parentheses + Enabled: true # Checks if uses of quotes match the configured preference. Style/StringLiterals: @@ -599,7 +631,9 @@ Style/StringLiteralsInInterpolation: # Checks if configured preferred methods are used over non-preferred. Style/StringMethods: - Enabled: false + PreferredMethods: + intern: to_sym + Enabled: true # Use %i or %I for arrays of symbols. Style/SymbolArray: @@ -657,23 +691,24 @@ Style/UnneededPercentQ: # Don't interpolate global, instance and class variables directly in strings. Style/VariableInterpolation: - Enabled: false + Enabled: true # Use the configured style when naming variables. Style/VariableName: - Enabled: false + EnforcedStyle: snake_case + Enabled: true # Use when x then ... for one-line cases. Style/WhenThen: - Enabled: false + Enabled: true # Checks for redundant do after while or until. Style/WhileUntilDo: - Enabled: false + Enabled: true # Favor modifier while/until usage when you have a single-line body. Style/WhileUntilModifier: - Enabled: false + Enabled: true # Use %w or %W for arrays of words. Style/WordArray: @@ -749,28 +784,28 @@ Lint/AssignmentInCondition: # Align block ends correctly. Lint/BlockAlignment: - Enabled: false + Enabled: true # Default values in optional keyword arguments and optional ordinal arguments # should not refer back to the name of the argument. Lint/CircularArgumentReference: - Enabled: false + Enabled: true # Checks for condition placed in a confusing position relative to the keyword. Lint/ConditionPosition: - Enabled: false + Enabled: true # Check for debugger calls. Lint/Debugger: - Enabled: false + Enabled: true # Align ends corresponding to defs correctly. Lint/DefEndAlignment: - Enabled: false + Enabled: true # Check for deprecated class method calls. Lint/DeprecatedClassMethods: - Enabled: false + Enabled: true # Check for duplicate method definitions. Lint/DuplicateMethods: @@ -782,15 +817,15 @@ Lint/DuplicatedKey: # Check for immutable argument given to each_with_object. Lint/EachWithObjectArgument: - Enabled: false + Enabled: true # Check for odd code arrangement in an else block. Lint/ElseLayout: - Enabled: false + Enabled: true # Checks for empty ensure block. Lint/EmptyEnsure: - Enabled: false + Enabled: true # Checks for empty string interpolation. Lint/EmptyInterpolation: @@ -798,37 +833,36 @@ Lint/EmptyInterpolation: # Align ends correctly. Lint/EndAlignment: - Enabled: false + Enabled: true # END blocks should not be placed inside method definitions. Lint/EndInMethod: - Enabled: false + Enabled: true # Do not use return in an ensure block. Lint/EnsureReturn: - Enabled: false + Enabled: true # The use of eval represents a serious security risk. Lint/Eval: - Enabled: false + Enabled: true # Catches floating-point literals too large or small for Ruby to represent. Lint/FloatOutOfRange: - Enabled: false + Enabled: true # The number of parameters to format/sprint must match the fields. Lint/FormatParameterMismatch: - Enabled: false + Enabled: true # Don't suppress exception. Lint/HandleExceptions: Enabled: false -# TODO: Enable ImplicitStringConcatenation Cop. # Checks for adjacent string literals on the same line, which could better be # represented as a single string literal. Lint/ImplicitStringConcatenation: - Enabled: false + Enabled: true # TODO: Enable IneffectiveAccessModifier Cop. # Checks for attempts to use `private` or `protected` to set the visibility @@ -839,15 +873,15 @@ Lint/IneffectiveAccessModifier: # Checks for invalid character literals with a non-escaped whitespace # character. Lint/InvalidCharacterLiteral: - Enabled: false + Enabled: true # Checks of literals used in conditions. Lint/LiteralInCondition: - Enabled: false + Enabled: true # Checks for literals used in interpolation. Lint/LiteralInInterpolation: - Enabled: false + Enabled: true # Use Kernel#loop with break rather than begin/end/until or begin/end/while # for post-loop tests. @@ -856,11 +890,11 @@ Lint/Loop: # Do not use nested method definitions. Lint/NestedMethodDefinition: - Enabled: false + Enabled: true # Do not omit the accumulator when calling `next` in a `reduce`/`inject` block. Lint/NextWithoutAccumulator: - Enabled: false + Enabled: true # Checks for method calls with a space before the opening parenthesis. Lint/ParenthesesAsGroupedExpression: @@ -869,11 +903,11 @@ Lint/ParenthesesAsGroupedExpression: # Checks for `rand(1)` calls. Such calls always return `0` and most likely # a mistake. Lint/RandOne: - Enabled: false + Enabled: true # Use parentheses in the method call to avoid confusion about precedence. Lint/RequireParentheses: - Enabled: false + Enabled: true # Avoid rescuing the Exception class. Lint/RescueException: @@ -908,7 +942,7 @@ Lint/UnusedMethodArgument: # Unreachable code. Lint/UnreachableCode: - Enabled: false + Enabled: true # Checks for useless access modifiers. Lint/UselessAccessModifier: @@ -920,33 +954,31 @@ Lint/UselessAssignment: # Checks for comparison of something with itself. Lint/UselessComparison: - Enabled: false + Enabled: true # Checks for useless `else` in `begin..end` without `rescue`. Lint/UselessElseWithoutRescue: - Enabled: false + Enabled: true # Checks for useless setter call to a local variable. Lint/UselessSetterCall: - Enabled: false + Enabled: true # Possible use of operator/literal/variable in void context. Lint/Void: - Enabled: false + Enabled: true ##################### Performance ############################ -# TODO: Enable Casecmp Cop. # Use `casecmp` rather than `downcase ==`. Performance/Casecmp: - Enabled: false + Enabled: true -# TODO: Enable DoubleStartEndWith Cop. # Use `str.{start,end}_with?(x, ..., y, ...)` instead of # `str.{start,end}_with?(x, ...) || str.{start,end}_with?(y, ...)`. Performance/DoubleStartEndWith: - Enabled: false + Enabled: true # TODO: Enable EndWith Cop. # Use `end_with?` instead of a regex match anchored to the end of a string. @@ -957,10 +989,9 @@ Performance/EndWith: Performance/LstripRstrip: Enabled: true -# TODO: Enable RangeInclude Cop. # Use `Range#cover?` instead of `Range#include?`. Performance/RangeInclude: - Enabled: false + Enabled: true # TODO: Enable RedundantBlockCall Cop. # Use `yield` instead of `block.call`. @@ -980,26 +1011,24 @@ Performance/RedundantMerge: MaxKeyValuePairs: 2 Enabled: false -# TODO: Enable RedundantSortBy Cop. # Use `sort` instead of `sort_by { |x| x }`. Performance/RedundantSortBy: - Enabled: false + Enabled: true -# TODO: Enable StartWith Cop. # Use `start_with?` instead of a regex match anchored to the beginning of a # string. Performance/StartWith: - Enabled: false + Enabled: true + # Use `tr` instead of `gsub` when you are replacing the same number of # characters. Use `delete` instead of `gsub` when you are deleting # characters. Performance/StringReplacement: - Enabled: false + Enabled: true -# TODO: Enable TimesMap Cop. # Checks for `.times.map` calls. Performance/TimesMap: - Enabled: false + Enabled: true ##################### Rails ################################## @@ -1024,11 +1053,11 @@ Rails/Delegate: # Prefer `find_by` over `where.first`. Rails/FindBy: - Enabled: false + Enabled: true # Prefer `all.find_each` over `all.find`. Rails/FindEach: - Enabled: false + Enabled: true # Prefer has_many :through to has_and_belongs_to_many. Rails/HasAndBelongsToMany: @@ -1040,7 +1069,7 @@ Rails/Output: # Checks for incorrect grammar when using methods like `3.day.ago`. Rails/PluralizationGrammar: - Enabled: false + Enabled: true # Checks for `read_attribute(:attr)` and `write_attribute(:attr, val)`. Rails/ReadWriteAttribute: @@ -1048,7 +1077,7 @@ Rails/ReadWriteAttribute: # Checks the arguments of ActiveRecord scopes. Rails/ScopeArgs: - Enabled: false + Enabled: true # Checks the correct usage of time zone aware methods. # http://danilenko.org/2012/7/6/rails_timezones @@ -1058,3 +1087,65 @@ Rails/TimeZone: # Use validates :attribute, hash of validations. Rails/Validation: Enabled: false + +##################### RSpec ################################## + +# Check that instances are not being stubbed globally. +RSpec/AnyInstance: + Enabled: false + +# Check that the first argument to the top level describe is the tested class or +# module. +RSpec/DescribeClass: + Enabled: false + +# Use `described_class` for tested class / module. +RSpec/DescribeMethod: + Enabled: false + +# Checks that the second argument to top level describe is the tested method +# name. +RSpec/DescribedClass: + Enabled: false + +# Checks for long example. +RSpec/ExampleLength: + Enabled: false + Max: 5 + +# Do not use should when describing your tests. +RSpec/ExampleWording: + Enabled: false + CustomTransform: + be: is + have: has + not: does not + IgnoredWords: [] + +# Checks the file and folder naming of the spec file. +RSpec/FilePath: + Enabled: false + CustomTransform: + RuboCop: rubocop + RSpec: rspec + +# Checks if there are focused specs. +RSpec/Focus: + Enabled: true + +# Checks for the usage of instance variables. +RSpec/InstanceVariable: + Enabled: false + +# Checks for multiple top-level describes. +RSpec/MultipleDescribes: + Enabled: false + +# Enforces the usage of the same method on all negative message expectations. +RSpec/NotToNot: + EnforcedStyle: not_to + Enabled: true + +# Prefer using verifying doubles over normal doubles. +RSpec/VerifiedDoubles: + Enabled: false diff --git a/.vagrant_enabled b/.vagrant_enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/CHANGELOG b/CHANGELOG index 822fa4be565..ec026b8f398 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,47 +1,180 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.8.0 (unreleased) +v 8.9.0 (unreleased) + - Allow enabling wiki page events from Webhook management UI + - Make EmailsOnPushWorker use Sidekiq mailers queue + - Fix wiki page events' webhook to point to the wiki repository + - Fix issue todo not remove when leave project !4150 (Long Nguyen) + - Allow forking projects with restricted visibility level + - Improve note validation to prevent errors when creating invalid note via API + - Reduce number of fog gem dependencies + - Remove project notification settings associated with deleted projects + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects + - Redesign navigation for project pages + - Fix groups API to list only user's accessible projects + - Redesign account and email confirmation emails + - Use gitlab-shell v3.0.0 + - Add DB index on users.state + - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database + - Changed the Slack build message to use the singular duration if necessary (Aran Koning) + - Fix issues filter when ordering by milestone + - Todos will display target state if issuable target is 'Closed' or 'Merged' + - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - Link to blank group icon doesn't throw a 404 anymore + - Remove 'main language' feature + - Pipelines can be canceled only when there are running builds + - Use downcased path to container repository as this is expected path by Docker + - Projects pending deletion will render a 404 page + - Measure queue duration between gitlab-workhorse and Rails + - Make authentication service for Container Registry to be compatible with < Docker 1.11 + - Add Application Setting to configure Container Registry token expire delay (default 5min) + - Cache assigned issue and merge request counts in sidebar nav + - Cache project build count in sidebar nav + - Reduce number of queries needed to render issue labels in the sidebar + - Improve error handling importing projects + - Put project Files and Commits tabs under Code tab + +v 8.8.3 + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 + - Fixed JS error when trying to remove discussion form. !4303 + - Fixed issue with button color when no CI enabled. !4287 + - Fixed potential issue with 2 CI status polling events happening. !3869 + - Improve design of Pipeline view. !4230 + - Fix gitlab importer failing to import new projects due to missing credentials. !4301 + - Fix import URL migration not rescuing with the correct Error. !4321 + - Fix health check access token changing due to old application settings being used. !4332 + - Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363 + - Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364 + - Pass the "Remember me" value to the 2FA token form. !4369 + - Fix incorrect links on pipeline page when merge request created from fork. !4376 + - Use downcased path to container repository as this is expected path by Docker. !4420 + - Fix wiki project clone address error (chujinjin). !4429 + - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 + - Fix missing number on generated ordered list element. !4437 + - Prevent disclosure of notes on confidential issues in search results. + +v 8.8.2 + - Added remove due date button. !4209 + - Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources. !4242 + - Fix Error 500 in CI charts by gracefully handling commits with no durations. !4245 + - Fix table UI on CI builds page. !4249 + - Fix backups if registry is disabled. !4263 + - Fixed issue with merge button color. !4211 + - Fixed issue with enter key selecting wrong option in dropdown. !4210 + - When creating a .gitignore file a dropdown with templates will be provided. !4075 + - Fix concurrent request when updating build log in browser. !4183 + +v 8.8.1 + - Add documentation for the "Health Check" feature + - Allow anonymous users to access a public project's pipelines !4233 + - Fix MySQL compatibility in zero downtime migrations helpers + - Fix the CI login to Container Registry (the gitlab-ci-token user) + +v 8.8.0 + - Implement GFM references for milestones (Alejandro Rodríguez) + - Snippets tab under user profile. !4001 (Long Nguyen) + - Fix error when using link to uploads in global snippets + - Fix Error 500 when attempting to retrieve project license when HEAD points to non-existent ref - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) + - Use a case-insensitive comparison in sanitizing URI schemes + - Toggle sign-up confirmation emails in application settings + - Make it possible to prevent tagged runner from picking untagged jobs + - Added `InlineDiffFilter` to the markdown parser. (Adam Butler) + - Added inline diff styling for `change_title` system notes. (Adam Butler) - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages + - Improve design of Pipeline View + - Fix scope used when accessing container registry + - Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios - Improve multiple branch push performance by memoizing permission checking - Log to application.log when an admin starts and stops impersonating a user + - Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi) - Updated gitlab_git to 10.1.0 - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists - Reduce delay in destroying a project from 1-minute to immediately - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 + - Added /health_check endpoint for checking service status + - Make 'upcoming' filter for milestones work better across projects - Sanitize repo paths in new project error message - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. - Support e-mail notifications for comments on project snippets + - Fix API leak of notes of unauthorized issues, snippets and merge requests - Use ActionDispatch Remote IP for Akismet checking - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project - Update SVG sanitizer to conform to SVG 1.1 + - Speed up push emails with multiple recipients by only generating the email once - Updated search UI + - Added authentication service for Container Registry - Display informative message when new milestone is created + - Sanitize milestones and labels titles + - Support multi-line tag messages. !3833 (Calin Seciu) + - Force users to reset their password after an admin changes it - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE - Create tags using Rugged for performance reasons. !3745 + - Allow guests to set notification level in projects + - API: Expose Issue#user_notes_count. !3126 (Anton Popov) + - Don't show forks button when user can't view forks + - Fix atom feed links and rendering - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) + - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 - Added multiple colors for labels in dropdowns when dups happen. + - Show commits in the same order as `git log` - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) - Expire repository exists? and has_visible_content? caches after a push if necessary - - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) + - Fix unintentional filtering bug in Issue/MR sorted by milestone due (Takuya Noguchi) + - Fix adding a todo for private group members (Ahmad Sherif) + - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 + - Total method execution timings are no longer tracked + - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) + - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) + - Hide left sidebar on phone screens to give more space for content + - Redesign navigation for profile and group pages + - Add counter metrics for rails cache + - Import pull requests from GitHub where the source or target branches were removed + - All Grape API helpers are now instrumented + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) + - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) + - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs) + - When creating a .gitignore file a dropdown with templates will be provided + +v 8.7.7 + - Fix import by `Any Git URL` broken if the URL contains a space + +v 8.7.6 + - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + - Fix import from GitLab.com to a private instance failure. !4181 + - Fix external imports not finding the import data. !4106 + - Fix notification delay when changing status of an issue + +v 8.7.5 + - Fix relative links in wiki pages. !4050 + - Fix always showing build notification message when switching between merge requests !4086 + - Fix an issue when filtering merge requests with more than one label. !3886 + - Fix short note for the default scope on build page (Takuya Noguchi) v 8.7.4 - - Fix always showing build notification message when switching between merge requests + - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) + - Fix setting trusted proxies !3970 + - Fix BitBucket importer bug when throwing exceptions !3941 + - Use sign out path only if not empty !3989 + - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 + - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 + - Use a case-insensitive comparison in sanitizing URI schemes v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented - Merge request widget displays TeamCity build state and code coverage correctly again. - Fix the line code when importing PR review comments from GitHub. !4010 - Wikis are now initialized on legacy projects when checking repositories + - Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea) v 8.7.2 - The "New Branch" button is now loaded asynchronously @@ -850,7 +983,7 @@ v 8.1.3 - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication -v 8.1.2 +v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory - Fix specific runners visibility @@ -1475,20 +1608,17 @@ v 7.10.0 - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - Fix merge request comments on files with multiple commits - Fix Resource Owner Password Authentication Flow - -v 7.9.4 - - Security: Fix project import URL regex to prevent arbitary local repos from being imported - - Fixed issue where only 25 commits would load in file listings - - Fix LDAP identities after config update - -v 7.9.3 - - Contains no changes - Add icons to Add dropdown items. - Allow admin to create public deploy keys that are accessible to any project. - Warn when gitlab-shell version doesn't match requirement. - Skip email confirmation when set by admin or via LDAP. - Only allow users to reference groups, projects, issues, MRs, commits they have access to. +v 7.9.4 + - Security: Fix project import URL regex to prevent arbitary local repos from being imported + - Fixed issue where only 25 commits would load in file listings + - Fix LDAP identities after config update + v 7.9.3 - Contains no changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fe4cf7b0f6..a15f8c4fec7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -311,13 +311,11 @@ request is as follows: 1. Create a feature branch 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) -1. If you are changing the README, some documentation or other things which - have no effect on the tests, add `[ci skip]` somewhere in the commit message - and make sure to read the [documentation styleguide][doc-styleguide] +1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] 1. If you have multiple commits please combine them into one commit by [squashing them][git-squash] 1. Push the commit(s) to your fork -1. Submit a merge request (MR) to the master branch +1. Submit a merge request (MR) to the `master` branch 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, see the [merge request description format] diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 37c2961c243..4a36342fcab 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.7.2 +3.0.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 39e898a4f95..0a1ffad4b4d 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.1 +0.7.4 diff --git a/Gemfile b/Gemfile index 1137ef4d72b..d9429de786f 100644 --- a/Gemfile +++ b/Gemfile @@ -18,9 +18,8 @@ gem "mysql2", '~> 0.3.16', group: :mysql gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem 'devise', '~> 3.5.4' +gem 'devise', '~> 4.0' gem 'doorkeeper', '~> 3.1' -gem 'devise-async', '~> 0.9.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' @@ -36,15 +35,16 @@ gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' +gem 'jwt' # Spam and anti-bot protection gem 'recaptcha', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' # Two-factor authentication -gem 'devise-two-factor', '~> 2.0.0' +gem 'devise-two-factor', '~> 3.0.0' gem 'rqrcode-rails3', '~> 0.1.7' -gem 'attr_encrypted', '~> 1.3.4' +gem 'attr_encrypted', '~> 3.0.0' # Browser detection gem "browser", '~> 1.0.0' @@ -72,7 +72,7 @@ gem 'grape-entity', '~> 0.4.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination -gem "kaminari", "~> 0.16.3" +gem "kaminari", "~> 0.17.0" # HAML gem "haml-rails", '~> 0.9.0' @@ -83,8 +83,14 @@ gem "carrierwave", '~> 0.10.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' +# for backups +gem 'fog-aws', '~> 0.9' +gem 'fog-core', '~> 1.40' +gem 'fog-local', '~> 0.3' +gem 'fog-google', '~> 0.3' +gem 'fog-openstack', '~> 0.1' + # for aws storage -gem "fog", "~> 1.36.0" gem "unf", '~> 0.1.4' # Authorization @@ -120,7 +126,7 @@ group :unicorn do end # State machine -gem "state_machines-activerecord", '~> 0.3.0' +gem "state_machines-activerecord", '~> 0.4.0' # Run events after state machine commits gem 'after_commit_queue' @@ -177,9 +183,6 @@ gem 'ruby-fogbugz', '~> 0.2.1' # d3 gem 'd3_rails', '~> 3.5.0' -#cal-heatmap -gem 'cal-heatmap-rails', '~> 3.6.0' - # underscore-rails gem "underscore-rails", "~> 1.8.0" @@ -197,7 +200,7 @@ gem 'licensee', '~> 8.0.0' gem "rack-attack", '~> 4.3.1' # Ace editor -gem 'ace-rails-ap', '~> 2.0.1' +gem 'ace-rails-ap', '~> 4.0.2' # Keyboard shortcuts gem 'mousetrap-rails', '~> 1.4.6' @@ -218,13 +221,13 @@ gem 'gitlab_emoji', '~> 0.3.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 4.1.0' -gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 0.15' @@ -242,7 +245,6 @@ group :development do gem "foreman" gem 'brakeman', '~> 3.2.0', require: false - gem "annotate", "~> 2.7.0" gem 'letter_opener_web', '~> 1.3.0' gem 'quiet_assets', '~> 1.0.2' gem 'rerun', '~> 0.11.0' @@ -293,9 +295,10 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.38.0', require: false + gem 'rubocop', '~> 0.40.0', require: false + gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false - gem 'coveralls', '~> 0.8.2', require: false + gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.11.0', require: false gem 'flog', require: false gem 'flay', require: false @@ -325,8 +328,7 @@ gem "mail_room", "~> 0.7" gem 'email_reply_parser', '~> 0.5.8' ## CI -gem 'activerecord-deprecated_finders', '~> 1.0.3' -gem 'activerecord-session_store', '~> 0.1.0' +gem 'activerecord-session_store', '~> 1.0.0' gem "nested_form", '~> 0.3.2' # OAuth @@ -334,3 +336,6 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" + +# Health check +gem 'health_check', '~> 1.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index fe083c2b566..8ae25269e65 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,8 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.2) RedCloth (4.2.9) - ace-rails-ap (2.0.1) + ace-rails-ap (4.0.2) actionmailer (4.2.6) actionpack (= 4.2.6) actionview (= 4.2.6) @@ -33,11 +32,12 @@ GEM activemodel (= 4.2.6) activesupport (= 4.2.6) arel (~> 6.0) - activerecord-deprecated_finders (1.0.4) - activerecord-session_store (0.1.2) - actionpack (>= 4.0.0, < 5) - activerecord (>= 4.0.0, < 5) - railties (>= 4.0.0, < 5) + activerecord-session_store (1.0.0) + actionpack (>= 4.0, < 5.1) + activerecord (>= 4.0, < 5.1) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 1.5.2, < 3) + railties (>= 4.0, < 5.1) activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) @@ -51,9 +51,6 @@ GEM activerecord (>= 3.0) akismet (2.0.0) allocations (1.0.4) - annotate (2.7.0) - activerecord (>= 3.2, < 6.0) - rake (~> 10.4) arel (6.0.3) asana (0.4.0) faraday (~> 0.9) @@ -62,8 +59,8 @@ GEM oauth2 (~> 1.0) asciidoctor (1.5.3) ast (2.2.0) - attr_encrypted (1.3.4) - encryptor (>= 1.3.0) + attr_encrypted (3.0.1) + encryptor (~> 3.0.0) attr_required (1.0.0) autoprefixer-rails (6.2.3) execjs @@ -74,7 +71,8 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) - bcrypt (3.1.10) + base32 (0.3.2) + bcrypt (3.1.11) benchmark-ips (2.3.0) better_errors (1.0.1) coderay (>= 1.0.0) @@ -103,7 +101,6 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) - cal-heatmap-rails (3.6.0) capybara (2.6.2) addressable mime-types (>= 1.16) @@ -157,21 +154,18 @@ GEM activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.5.4) + devise (4.1.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 5.1) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) - devise-two-factor (2.0.1) + devise-two-factor (3.0.0) activesupport - attr_encrypted (~> 1.3.2) - devise (~> 3.5.0) + attr_encrypted (>= 1.3, < 4, != 2) + devise (~> 4.0) railties - rotp (~> 2) + rotp (~> 2.0) diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) @@ -183,12 +177,12 @@ GEM email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) - encryptor (1.3.0) + encryptor (3.0.0) equalizer (0.0.11) erubis (2.7.0) escape_utils (1.1.1) eventmachine (1.0.8) - excon (0.45.4) + excon (0.49.0) execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.5.0) @@ -205,8 +199,6 @@ GEM multi_json ffaker (2.0.0) ffi (1.9.10) - fission (0.5.0) - CFPropertyList (~> 2.2) flay (2.6.1) ruby_parser (~> 3.0) sexp_processor (~> 4.0) @@ -216,109 +208,28 @@ GEM flowdock (0.7.1) httparty (~> 0.7) multi_json - fog (1.36.0) - fog-aliyun (>= 0.1.0) - fog-atmos - fog-aws (>= 0.6.0) - fog-brightbox (~> 0.4) - fog-core (~> 1.32) - fog-dynect (~> 0.0.2) - fog-ecloud (~> 0.1) - fog-google (<= 0.1.0) - fog-json - fog-local - fog-powerdns (>= 0.1.1) - fog-profitbricks - fog-radosgw (>= 0.0.2) - fog-riakcs - fog-sakuracloud (>= 0.0.4) - fog-serverlove - fog-softlayer - fog-storm_on_demand - fog-terremark - fog-vmfusion - fog-voxel - fog-xenserver - fog-xml (~> 0.1.1) - ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-aliyun (0.1.0) - fog-core (~> 1.27) - fog-json (~> 1.0) - ipaddress (~> 0.8) - xml-simple (~> 1.1) - fog-atmos (0.1.0) - fog-core - fog-xml - fog-aws (0.8.1) + fog-aws (0.9.2) fog-core (~> 1.27) fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-brightbox (0.10.1) - fog-core (~> 1.22) - fog-json - inflecto (~> 0.0.2) - fog-core (1.35.0) + fog-core (1.40.0) builder - excon (~> 0.45) + excon (~> 0.49) formatador (~> 0.2) - fog-dynect (0.0.2) - fog-core - fog-json - fog-xml - fog-ecloud (0.3.0) - fog-core - fog-xml - fog-google (0.1.0) + fog-google (0.3.2) fog-core fog-json fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-local (0.2.1) - fog-core (~> 1.27) - fog-powerdns (0.1.1) + fog-local (0.3.0) fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) - fog-profitbricks (0.0.5) - fog-core - fog-xml - nokogiri - fog-radosgw (0.0.5) - fog-core (>= 1.21.0) - fog-json - fog-xml (>= 0.0.1) - fog-riakcs (0.1.0) - fog-core - fog-json - fog-xml - fog-sakuracloud (1.7.5) - fog-core - fog-json - fog-serverlove (0.1.2) - fog-core - fog-json - fog-softlayer (1.0.3) - fog-core - fog-json - fog-storm_on_demand (0.1.1) - fog-core - fog-json - fog-terremark (0.1.0) - fog-core - fog-xml - fog-vmfusion (0.1.0) - fission - fog-core - fog-voxel (0.1.0) - fog-core - fog-xml - fog-xenserver (0.2.2) - fog-core - fog-xml + fog-openstack (0.1.6) + fog-core (>= 1.39) + fog-json (>= 1.0) + ipaddress (>= 0.8) fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) @@ -405,6 +316,8 @@ GEM html2haml (>= 1.0.1) railties (>= 4.0.1) hashie (3.4.3) + health_check (1.5.1) + rails (>= 2.3.0) highline (1.7.8) hipchat (1.5.2) httparty @@ -425,18 +338,15 @@ GEM httpclient (2.7.0.1) i18n (0.7.0) ice_nine (0.11.1) - inflecto (0.0.2) influxdb (0.2.3) cause json - ipaddress (0.8.2) + ipaddress (0.8.3) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-scrollto-rails (1.4.3) - railties (> 3.1, < 5.0) jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks @@ -444,7 +354,7 @@ GEM railties (>= 3.2.16) json (1.8.3) jwt (1.5.2) - kaminari (0.16.3) + kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.10.0) @@ -552,7 +462,7 @@ GEM orm_adapter (0.5.0) paranoia (2.1.4) activerecord (~> 4.0) - parser (2.3.0.6) + parser (2.3.1.0) ast (~> 2.2) pg (0.18.4) poltergeist (1.9.0) @@ -658,7 +568,7 @@ GEM responders (2.1.1) railties (>= 4.2.0, < 5.1) rinku (1.7.3) - rotp (2.1.1) + rotp (2.1.2) rouge (1.10.1) rqrcode (0.7.0) chunky_png @@ -687,15 +597,17 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.4.1) - rubocop (0.38.0) - parser (>= 2.3.0.6, < 3.0) + rubocop (0.40.0) + parser (>= 2.3.1.0, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-rspec (1.5.0) + rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) - ruby-progressbar (1.7.5) + ruby-progressbar (1.8.1) ruby-saml (1.1.2) nokogiri (>= 1.5.10) uuid (~> 2.3) @@ -789,11 +701,11 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) state_machines (0.4.0) - state_machines-activemodel (0.3.0) - activemodel (~> 4.1) + state_machines-activemodel (0.4.0) + activemodel (>= 4.1, < 5.1) state_machines (>= 0.4.0) - state_machines-activerecord (0.3.0) - activerecord (~> 4.1) + state_machines-activerecord (0.4.0) + activerecord (>= 4.1, < 5.1) state_machines-activemodel (>= 0.3.0) stringex (2.5.2) systemu (2.6.5) @@ -842,7 +754,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.0.2) + unicode-display_width (1.0.5) unicorn (4.9.0) kgio (~> 2.6) rack @@ -859,7 +771,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - warden (1.2.4) + warden (1.2.6) rack (>= 1.0) web-console (2.3.0) activemodel (>= 4.0) @@ -876,7 +788,6 @@ GEM builder expression_parser rinku - xml-simple (1.1.5) xpath (2.0.0) nokogiri (~> 1.3) @@ -885,20 +796,19 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) - ace-rails-ap (~> 2.0.1) - activerecord-deprecated_finders (~> 1.0.3) - activerecord-session_store (~> 0.1.0) + ace-rails-ap (~> 4.0.2) + activerecord-session_store (~> 1.0.0) acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) after_commit_queue akismet (~> 2.0) allocations (~> 1.0) - annotate (~> 2.7.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) - attr_encrypted (~> 1.3.4) + attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) + base32 (~> 0.3.0) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) @@ -908,7 +818,6 @@ DEPENDENCIES bullet bundler-audit byebug - cal-heatmap-rails (~> 3.6.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.10.0) @@ -921,9 +830,8 @@ DEPENDENCIES d3_rails (~> 3.5.0) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) - devise (~> 3.5.4) - devise-async (~> 0.9.0) - devise-two-factor (~> 2.0.0) + devise (~> 4.0) + devise-two-factor (~> 3.0.0) diffy (~> 3.0.3) doorkeeper (~> 3.1) dropzonejs-rails (~> 0.7.1) @@ -933,7 +841,11 @@ DEPENDENCIES ffaker (~> 2.0.0) flay flog - fog (~> 1.36.0) + fog-aws (~> 0.9) + fog-core (~> 1.40) + fog-google (~> 0.3) + fog-local (~> 0.3) + fog-openstack (~> 0.1) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) @@ -951,16 +863,17 @@ DEPENDENCIES grape (~> 0.13.0) grape-entity (~> 0.4.2) haml-rails (~> 0.9.0) + health_check (~> 1.5.1) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) influxdb (~> 0.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) - kaminari (~> 0.16.3) + jwt + kaminari (~> 0.17.0) letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) loofah (~> 2.0.3) @@ -1016,7 +929,8 @@ DEPENDENCIES rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.4.0) rspec-retry - rubocop (~> 0.38.0) + rubocop (~> 0.40.0) + rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) sass-rails (~> 5.0.0) @@ -1041,7 +955,7 @@ DEPENDENCIES spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.6.0) - state_machines-activerecord (~> 0.3.0) + state_machines-activerecord (~> 0.4.0) task_list (~> 1.0.2) teaspoon (~> 1.1.0) teaspoon-jasmine (~> 2.2.0) @@ -1061,4 +975,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.12.1 + 1.12.4 diff --git a/README.md b/README.md index 5e41665a6ba..fee93d5f9c3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # GitLab [![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) -[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) -[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) ## Canonical source @@ -35,11 +33,11 @@ There are two editions of GitLab: On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: -- [Subscriptions](https://about.gitlab.com/subscription/) +- [Subscriptions](https://about.gitlab.com/pricing/) - [Consultancy](https://about.gitlab.com/consultancy/) - [Community](https://about.gitlab.com/community/) - [Hosted GitLab.com](https://about.gitlab.com/gitlab-com/) use GitLab as a free service -- [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/) with additional features aimed at larger organizations. +- [GitLab Enterprise Edition](https://about.gitlab.com/features/#enterprise) with additional features aimed at larger organizations. - [GitLab CI](https://about.gitlab.com/gitlab-ci/) a continuous integration (CI) server that is easy to integrate with GitLab. ## Requirements diff --git a/VERSION b/VERSION index d5a967c3933..6c07f656285 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.8.0-pre +8.9.0-pre diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg deleted file mode 100644 index 0e05674e840..00000000000 Binary files a/app/assets/images/ci/arch.jpg and /dev/null differ diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico deleted file mode 100644 index 9663d4d00b9..00000000000 Binary files a/app/assets/images/ci/favicon.ico and /dev/null differ diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif deleted file mode 100644 index 2fcb8f2da0d..00000000000 Binary files a/app/assets/images/ci/loader.gif and /dev/null differ diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png deleted file mode 100644 index 752d26adba7..00000000000 Binary files a/app/assets/images/ci/no_avatar.png and /dev/null differ diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png deleted file mode 100644 index d5edc04e65f..00000000000 Binary files a/app/assets/images/ci/rails.png and /dev/null differ diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png deleted file mode 100644 index 65d29e3fd89..00000000000 Binary files a/app/assets/images/ci/service_sample.png and /dev/null differ diff --git a/app/assets/images/mailers/gitlab_header_logo.png b/app/assets/images/mailers/gitlab_header_logo.png new file mode 100644 index 00000000000..35ca1860887 Binary files /dev/null and b/app/assets/images/mailers/gitlab_header_logo.png differ diff --git a/app/assets/images/mailers/gitlab_tanuki_2x.png b/app/assets/images/mailers/gitlab_tanuki_2x.png new file mode 100644 index 00000000000..551dd6ce2ce Binary files /dev/null and b/app/assets/images/mailers/gitlab_tanuki_2x.png differ diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index dd1bbb37551..3f61ea1eaf4 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -1,14 +1,15 @@ @Api = - groups_path: "/api/:version/groups.json" - group_path: "/api/:version/groups/:id.json" - namespaces_path: "/api/:version/namespaces.json" - group_projects_path: "/api/:version/groups/:id/projects.json" - projects_path: "/api/:version/projects.json" - labels_path: "/api/:version/projects/:id/labels" - license_path: "/api/:version/licenses/:key" + groupsPath: "/api/:version/groups.json" + groupPath: "/api/:version/groups/:id.json" + namespacesPath: "/api/:version/namespaces.json" + groupProjectsPath: "/api/:version/groups/:id/projects.json" + projectsPath: "/api/:version/projects.json" + labelsPath: "/api/:version/projects/:id/labels" + licensePath: "/api/:version/licenses/:key" + gitignorePath: "/api/:version/gitignores/:key" group: (group_id, callback) -> - url = Api.buildUrl(Api.group_path) + url = Api.buildUrl(Api.groupPath) url = url.replace(':id', group_id) $.ajax( @@ -22,7 +23,7 @@ # Return groups list. Filtered by query # Only active groups retrieved groups: (query, skip_ldap, callback) -> - url = Api.buildUrl(Api.groups_path) + url = Api.buildUrl(Api.groupsPath) $.ajax( url: url @@ -36,7 +37,7 @@ # Return namespaces list. Filtered by query namespaces: (query, callback) -> - url = Api.buildUrl(Api.namespaces_path) + url = Api.buildUrl(Api.namespacesPath) $.ajax( url: url @@ -50,7 +51,7 @@ # Return projects list. Filtered by query projects: (query, order, callback) -> - url = Api.buildUrl(Api.projects_path) + url = Api.buildUrl(Api.projectsPath) $.ajax( url: url @@ -64,7 +65,7 @@ callback(projects) newLabel: (project_id, data, callback) -> - url = Api.buildUrl(Api.labels_path) + url = Api.buildUrl(Api.labelsPath) url = url.replace(':id', project_id) data.private_token = gon.api_token @@ -80,7 +81,7 @@ # Return group projects list. Filtered by query groupProjects: (group_id, query, callback) -> - url = Api.buildUrl(Api.group_projects_path) + url = Api.buildUrl(Api.groupProjectsPath) url = url.replace(':id', group_id) $.ajax( @@ -95,7 +96,7 @@ # Return text for a specific license licenseText: (key, data, callback) -> - url = Api.buildUrl(Api.license_path).replace(':key', key) + url = Api.buildUrl(Api.licensePath).replace(':key', key) $.ajax( url: url @@ -103,6 +104,12 @@ ).done (license) -> callback(license) + gitignoreText: (key, callback) -> + url = Api.buildUrl(Api.gitignorePath).replace(':key', key) + + $.get url, (gitignore) -> + callback(gitignore) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 5bac8eef1cb..18c1aa0d4e2 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -18,8 +18,6 @@ #= require jquery.atwho #= require jquery.scrollTo #= require jquery.turbolinks -#= require d3 -#= require cal-heatmap #= require turbolinks #= require autosave #= require bootstrap/affix @@ -52,7 +50,13 @@ #= require shortcuts_network #= require jquery.nicescroll #= require date.format -#= require_tree . +#= require_directory ./behaviors +#= require_directory ./blob +#= require_directory ./ci +#= require_directory ./commit +#= require_directory ./extensions +#= require_directory ./lib +#= require_directory . #= require fuzzaldrin-plus #= require cropper @@ -204,6 +208,7 @@ $ -> $('.header-content .title').toggle() $('.header-content .navbar-collapse').toggle() $('.navbar-toggle').toggleClass('active') + $('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left") # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> @@ -245,38 +250,6 @@ $ -> if $navIcon.hasClass('fa-angle-left') $navIconToggle.trigger('click') - $(document) - .off 'click', '.js-sidebar-toggle' - .on 'click', '.js-sidebar-toggle', (e, triggered) -> - e.preventDefault() - $this = $(this) - $thisIcon = $this.find 'i' - $allGutterToggleIcons = $('.js-sidebar-toggle i') - if $thisIcon.hasClass('fa-angle-double-right') - $allGutterToggleIcons - .removeClass('fa-angle-double-right') - .addClass('fa-angle-double-left') - $('aside.right-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - $('.page-with-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - else - $allGutterToggleIcons - .removeClass('fa-angle-double-left') - .addClass('fa-angle-double-right') - $('aside.right-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - $('.page-with-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - if not triggered - $.cookie("collapsed_gutter", - $('.right-sidebar') - .hasClass('right-sidebar-collapsed'), { path: '/' }) - fitSidebarForSize = -> oldBootstrapBreakpoint = bootstrapBreakpoint bootstrapBreakpoint = bp.getBreakpointSize() diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee new file mode 100644 index 00000000000..cc8a497d081 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee @@ -0,0 +1,58 @@ +class @BlobGitignoreSelector + constructor: (opts) -> + { + @dropdown + @editor + @$wrapper = @dropdown.closest('.gitignore-selector') + @$filenameInput = $('#file_name') + @data = @dropdown.data('filenames') + } = opts + + @dropdown.glDropdown( + data: @data, + filterable: true, + selectable: true, + search: + fields: ['name'] + clicked: @onClick + text: (gitignore) -> + gitignore.name + ) + + @toggleGitignoreSelector() + @bindEvents() + + bindEvents: -> + @$filenameInput + .on 'keyup blur', (e) => + @toggleGitignoreSelector() + + toggleGitignoreSelector: -> + filename = @$filenameInput.val() or $('.editor-file-name').text().trim() + @$wrapper.toggleClass 'hidden', filename isnt '.gitignore' + + onClick: (item, el, e) => + e.preventDefault() + @requestIgnoreFile(item.name) + + requestIgnoreFile: (name) -> + Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@) + + requestIgnoreFileSuccess: (gitignore) -> + @editor.setValue(gitignore.content, 1) + @editor.focus() + +class @BlobGitignoreSelectors + constructor: (opts) -> + { + @$dropdowns = $('.js-gitignore-selector') + @editor + } = opts + + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) + + new BlobGitignoreSelector( + dropdown: $dropdown, + editor: @editor + ) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index eea9aa972ee..79141e768b8 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -13,6 +13,7 @@ class @EditBlob @initModePanesAndLinks() new BlobLicenseSelector(@editor) + new BlobGitignoreSelectors(editor: @editor) initModePanesAndLinks: -> @$editModePanes = $(".js-edit-mode-pane") diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee deleted file mode 100644 index d80e0e716ce..00000000000 --- a/app/assets/javascripts/calendar.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -class @Calendar - constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> - cal = new CalHeatMap() - cal.init - itemName: ["contribution"] - data: timestamps - start: new Date(starting_year, starting_month) - domainLabelFormat: "%b" - id: "cal-heatmap" - domain: "month" - subDomain: "day" - range: 12 - tooltip: true - label: - position: "top" - legend: [ - 0 - 10 - 20 - 30 - ] - legendCellPadding: 3 - cellSize: $('.user-calendar').width() / 73 - onClick: (date, count) -> - formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() - $.ajax - url: calendar_activities_path - data: - date: formated_date - cache: false - dataType: "html" - success: (data) -> - $(".user-calendar-activities").html data - diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee index 05aa0f366bb..ca24c1d759f 100644 --- a/app/assets/javascripts/ci/application.js.coffee +++ b/app/assets/javascripts/ci/application.js.coffee @@ -1,34 +1,6 @@ -# This is a manifest file that'll be compiled into application.js, which will include all the files -# listed below. -# -# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -# -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. -# -# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD -# GO AFTER THE REQUIRES BELOW. -# #= require pager #= require jquery_nested_form #= require_tree . -# -$(document).on 'click', '.edit-runner-link', (event) -> - event.preventDefault() - - descr = $(this).closest('.runner-description').first() - descr.addClass('hide') - form = descr.next('.runner-description-form') - descrInput = form.find('input.description') - originalValue = descrInput.val() - form.removeClass('hide') - form.find('.cancel').on 'click', (event) -> - event.preventDefault() - - form.addClass('hide') - descrInput.val(originalValue) - descr.removeClass('hide') $(document).on 'click', '.assign-all-runner', -> $(this).replaceWith(' Assign in progress..') diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 7afe8bf79e2..98d05e41273 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -1,9 +1,12 @@ class CiBuild @interval: null + @state: null - constructor: (build_url, build_status) -> + constructor: (build_url, build_status, build_state) -> clearInterval(CiBuild.interval) + @state = build_state + @initScrollButtonAffix() if build_status == "running" || build_status == "pending" @@ -25,15 +28,22 @@ class CiBuild # CiBuild.interval = setInterval => if window.location.href.split("#").first() is build_url + last_state = @state $.ajax - url: build_url + url: build_url + "/trace.json?state=" + encodeURIComponent(@state) dataType: "json" - success: (build) => - if build.status == "running" - $('#build-trace code').html build.trace_html - $('#build-trace code').append '' + success: (log) => + return unless last_state is @state + + if log.state and log.status is "running" + @state = log.state + if log.append + $('.fa-refresh').before log.html + else + $('#build-trace code').html log.html + $('#build-trace code').append '' @checkAutoscroll() - else if build.status != build_status + else if log.status isnt build_status Turbolinks.visit build_url , 4000 diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f91aa3c5ad7..a3185f87640 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -16,7 +16,6 @@ class Dispatcher shortcut_handler = null switch page when 'projects:issues:index' - Issues.init() Issuable.init() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' @@ -119,7 +118,7 @@ class Dispatcher new UsersSelect() when 'projects' new NamespaceSelect() - when 'dashboard' + when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' new Profile() diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index a4304786cbb..3cc70185178 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -11,6 +11,7 @@ class @DueDateSelect $block = $dropdown.closest('.block') $selectbox = $dropdown.closest('.selectbox') $value = $block.find('.value') + $valueContent = $block.find('.value-content') $sidebarValue = $('.js-due-date-sidebar-value', $block) fieldName = $dropdown.data('field-name') @@ -23,11 +24,15 @@ class @DueDateSelect $value.removeAttr('style') ) - addDueDate = -> + addDueDate = (isDropdown) -> # Create the post date value = $("input[name='#{fieldName}']").val() - date = new Date value.replace(new RegExp('-', 'g'), ',') - mediumDate = $.datepicker.formatDate 'M d, yy', date + + if value isnt '' + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + else + mediumDate = 'None' data = {} data[abilityName] = {} @@ -39,23 +44,35 @@ class @DueDateSelect data: data beforeSend: -> $loading.fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $selectbox.hide() + if isDropdown + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() $value.removeAttr('style') - $value.html(mediumDate) + $valueContent.html(mediumDate) $sidebarValue.html(mediumDate) + + if value isnt '' + $('.js-remove-due-date-holder').removeClass 'hidden' + else + $('.js-remove-due-date-holder').addClass 'hidden' ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $dropdown.dropdown('toggle') + if isDropdown + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') $loading.fadeOut() + $block.on 'click', '.js-remove-due-date', (e) -> + e.preventDefault() + $("input[name='#{fieldName}']").val '' + addDueDate(false) + $datePicker.datepicker( dateFormat: 'yy-mm-dd', defaultDate: $("input[name='#{fieldName}']").val() altField: "input[name='#{fieldName}']" onSelect: -> - addDueDate() + addDueDate(true) ) $(document) diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 61e3f811e73..41dba342107 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -18,6 +18,10 @@ GitLab.GfmAutoComplete = Issues: template: '
  • ${id} ${title}
  • ' + # Milestones + Milestones: + template: '
  • ${title}
  • ' + # Add GFM auto-completion to all input fields, that accept GFM input. setup: (wrap) -> @input = $('.js-gfm-input') @@ -81,6 +85,19 @@ GitLab.GfmAutoComplete = title: sanitize(i.title) search: "#{i.iid} #{i.title}" + @input.atwho + at: '%' + alias: 'milestones' + searchKey: 'search' + displayTpl: @Milestones.template + insertTpl: '${atwho-at}"${title}"' + callbacks: + beforeSave: (milestones) -> + $.map milestones, (m) -> + id: m.iid + title: sanitize(m.title) + search: "#{m.title}" + @input.atwho at: '!' alias: 'mergerequests' @@ -105,6 +122,8 @@ GitLab.GfmAutoComplete = @input.atwho 'load', '@', data.members # load issues @input.atwho 'load', 'issues', data.issues + # load milestones + @input.atwho 'load', 'milestones', data.milestones # load merge requests @input.atwho 'load', 'mergerequests', data.mergerequests # load emojis diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1d1bfeb2e77..b3f1dc969b8 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -60,9 +60,36 @@ class GitLabDropdownFilter results = data if search_text isnt '' - results = fuzzaldrinPlus.filter(data, search_text, - key: @options.keys - ) + # When data is an array of objects therefore [object Array] e.g. + # [ + # { prop: 'foo' }, + # { prop: 'baz' } + # ] + if _.isArray(data) + results = fuzzaldrinPlus.filter(data, search_text, + key: @options.keys + ) + else + # If data is grouped therefore an [object Object]. e.g. + # { + # groupName1: [ + # { prop: 'foo' }, + # { prop: 'baz' } + # ], + # groupName2: [ + # { prop: 'abc' }, + # { prop: 'def' } + # ] + # } + if gl.utils.isObject data + results = {} + for key, group of data + tmp = fuzzaldrinPlus.filter(group, search_text, + key: @options.keys + ) + + if tmp.length + results[key] = tmp.map (item) -> item @options.callback results else @@ -141,8 +168,9 @@ class GitLabDropdown searchFields = if @options.search then @options.search.fields else []; if @options.data - # If data is an array - if _.isArray @options.data + # If we provided data + # data could be an array of objects or a group of arrays + if _.isObject(@options.data) and not _.isFunction(@options.data) @fullData = @options.data @parseData @options.data else @@ -230,19 +258,33 @@ class GitLabDropdown parseData: (data) -> @renderedData = data - # Render each row - html = $.map data, (obj) => - return @renderItem(obj) - if @options.filterable and data.length is 0 # render no matching results html = [@noResults()] + else + # Handle array groups + if gl.utils.isObject data + html = [] + for name, groupData of data + # Add header for each group + html.push(@renderItem(header: name, name)) + + @renderData(groupData, name) + .map (item) -> + html.push item + else + # Render each row + html = @renderData(data) # Render the full menu full_html = @renderMenu(html.join("")) @appendMenu(full_html) + renderData: (data, group = false) -> + data.map (obj, index) => + return @renderItem(obj, group, index) + shouldPropagate: (e) => if @options.multiSelect $target = $(e.target) @@ -299,11 +341,10 @@ class GitLabDropdown selector = '.dropdown-content' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one .dropdown-content" - $(selector, @dropdown).html html # Render the row - renderItem: (data) -> + renderItem: (data, group = false, index = false) -> html = "" # Divider @@ -346,8 +387,13 @@ class GitLabDropdown if @highlight text = @highlightTextMatches(text, @filterInput.val()) + if group + groupAttrs = "data-group='#{group}' data-index='#{index}'" + else + groupAttrs = '' + html = "
  • - + #{text}
  • " @@ -377,9 +423,15 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - selectedIndex = el.parent().index() if @renderedData - selectedObject = @renderedData[selectedIndex] + groupName = el.data('group') + if groupName + selectedIndex = el.data('index') + selectedObject = @renderedData[groupName][selectedIndex] + else + selectedIndex = el.closest('li').index() + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) @@ -460,7 +512,7 @@ class GitLabDropdown return false if currentKeyCode is 13 - @selectRowAtIndex currentIndex + @selectRowAtIndex if currentIndex < 0 then 0 else currentIndex removeArrayKeyEvent: -> $('body').off 'keydown' diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee new file mode 100644 index 00000000000..e0f681acf0b --- /dev/null +++ b/app/assets/javascripts/graphs/application.js.coffee @@ -0,0 +1,7 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require_tree . diff --git a/app/assets/javascripts/graphs/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee new file mode 100644 index 00000000000..f36c71fd25e --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph.js.coffee @@ -0,0 +1,6 @@ +class @StatGraph + @log: {} + @get_log: -> + @log + @set_log: (data) -> + @log = data diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee new file mode 100644 index 00000000000..1d9fae7cf79 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee @@ -0,0 +1,71 @@ +#= require d3 + +class @ContributorsStatGraph + init: (log) -> + @parsed_log = ContributorsStatGraphUtil.parse_log(log) + @set_current_field("commits") + total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field) + @add_master_graph(total_commits) + @add_authors_graph(author_commits) + @change_date_header() + add_master_graph: (total_data) -> + @master_graph = new ContributorsMasterGraph(total_data) + @master_graph.draw() + add_authors_graph: (author_data) -> + @authors = [] + limited_author_data = author_data.slice(0, 100) + _.each(limited_author_data, (d) => + author_header = @create_author_header(d) + $(".contributors-list").append(author_header) + @authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates) + author_graph.draw() + ) + format_author_commit_info: (author) -> + commits = $('', { + class: 'graph-author-commits-count' + }) + commits.text(author.commits + " commits") + $('').append(commits) + + create_author_header: (author) -> + list_item = $('
  • ', { + class: 'person' + style: 'display: block;' + }) + author_name = $('

    ' + author.author_name + '

    ') + author_email = $('

    ' + author.author_email + '

    ') + author_commit_info_span = $('', { + class: 'commits' + }) + author_commit_info = @format_author_commit_info(author) + author_commit_info_span.html(author_commit_info) + list_item.append(author_name) + list_item.append(author_email) + list_item.append(author_commit_info_span) + list_item + redraw_master: -> + total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) + @master_graph.set_data(total_data) + @master_graph.redraw() + redraw_authors: -> + $("ol").html("") + x_domain = ContributorsGraph.prototype.x_domain + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain) + _.each(author_commits, (d) => + @redraw_author_commit_info(d) + $(@authors[d.author_name].list_item).appendTo("ol") + @authors[d.author_name].set_data(d.dates) + @authors[d.author_name].redraw() + ) + set_current_field: (field) -> + @field = field + change_date_header: -> + x_domain = ContributorsGraph.prototype.x_domain + print_date_format = d3.time.format("%B %e %Y") + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]) + $("#date_header").text(print) + redraw_author_commit_info: (author) -> + author_list_item = $(@authors[author.author_name].list_item) + author_commit_info = @format_author_commit_info(author) + author_list_item.find("span").html(author_commit_info) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee new file mode 100644 index 00000000000..584d281a510 --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee @@ -0,0 +1,169 @@ +#= require d3 + +class @ContributorsGraph + MARGIN: + top: 20 + right: 20 + bottom: 30 + left: 50 + x_domain: null + y_domain: null + dates: [] + @set_x_domain: (data) => + @prototype.x_domain = data + @set_y_domain: (data) => + @prototype.y_domain = [0, d3.max(data, (d) -> + d.commits = d.commits ? d.additions ? d.deletions + )] + @init_x_domain: (data) => + @prototype.x_domain = d3.extent(data, (d) -> + d.date + ) + @init_y_domain: (data) => + @prototype.y_domain = [0, d3.max(data, (d) -> + d.commits = d.commits ? d.additions ? d.deletions + )] + @init_domain: (data) => + @init_x_domain(data) + @init_y_domain(data) + @set_dates: (data) => + @prototype.dates = data + set_x_domain: -> + @x.domain(@x_domain) + set_y_domain: -> + @y.domain(@y_domain) + set_domain: -> + @set_x_domain() + @set_y_domain() + create_scale: (width, height) -> + @x = d3.time.scale().range([0, width]).clamp(true) + @y = d3.scale.linear().range([height, 0]).nice() + draw_x_axis: -> + @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})") + .call(@x_axis) + draw_y_axis: -> + @svg.append("g").attr("class", "y axis").call(@y_axis) + set_data: (data) -> + @data = data + +class @ContributorsMasterGraph extends ContributorsGraph + constructor: (@data) -> + @width = $('.content').width() - 70 + @height = 200 + @x = null + @y = null + @x_axis = null + @y_axis = null + @area = null + @svg = null + @brush = null + @x_max_domain = null + process_dates: (data) -> + dates = @get_dates(data) + @parse_dates(data) + ContributorsGraph.set_dates(dates) + get_dates: (data) -> + _.pluck(data, 'date') + parse_dates: (data) -> + parseDate = d3.time.format("%Y-%m-%d").parse + data.forEach((d) -> + d.date = parseDate(d.date) + ) + create_scale: -> + super @width, @height + create_axes: -> + @x_axis = d3.svg.axis().scale(@x).orient("bottom") + @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) + create_svg: -> + @svg = d3.select("#contributors-master").append("svg") + .attr("width", @width + @MARGIN.left + @MARGIN.right) + .attr("height", @height + @MARGIN.top + @MARGIN.bottom) + .attr("class", "tint-box") + .append("g") + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") + create_area: (x, y) -> + @area = d3.svg.area().x((d) -> + x(d.date) + ).y0(@height).y1((d) -> + xa = d.commits = d.commits ? d.additions ? d.deletions + y(xa) + ).interpolate("basis") + create_brush: -> + @brush = d3.svg.brush().x(@x).on("brushend", @update_content) + draw_path: (data) -> + @svg.append("path").datum(data).attr("class", "area").attr("d", @area) + add_brush: -> + @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height) + update_content: => + ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent()) + $("#brush_change").trigger('change') + draw: -> + @process_dates(@data) + @create_scale() + @create_axes() + ContributorsGraph.init_domain(@data) + @x_max_domain = @x_domain + @set_domain() + @create_area(@x, @y) + @create_svg() + @create_brush() + @draw_path(@data) + @draw_x_axis() + @draw_y_axis() + @add_brush() + redraw: -> + @process_dates(@data) + ContributorsGraph.set_y_domain(@data) + @set_y_domain() + @svg.select("path").datum(@data) + @svg.select("path").attr("d", @area) + @svg.select(".y.axis").call(@y_axis) + +class @ContributorsAuthorGraph extends ContributorsGraph + constructor: (@data) -> + @width = $('.content').width()/2 - 100 + @height = 200 + @x = null + @y = null + @x_axis = null + @y_axis = null + @area = null + @svg = null + @list_item = null + create_scale: -> + super @width, @height + create_axes: -> + @x_axis = d3.svg.axis().scale(@x).orient("bottom").ticks(8) + @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) + create_area: (x, y) -> + @area = d3.svg.area().x((d) -> + parseDate = d3.time.format("%Y-%m-%d").parse + x(parseDate(d)) + ).y0(@height).y1((d) => + if @data[d]? then y(@data[d]) else y(0) + ).interpolate("basis") + create_svg: -> + @list_item = d3.selectAll(".person")[0].pop() + @svg = d3.select(@list_item).append("svg") + .attr("width", @width + @MARGIN.left + @MARGIN.right) + .attr("height", @height + @MARGIN.top + @MARGIN.bottom) + .attr("class", "spark") + .append("g") + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") + draw_path: (data) -> + @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area) + draw: -> + @create_scale() + @create_axes() + @set_domain() + @create_area(@x, @y) + @create_svg() + @draw_path(@dates) + @draw_x_axis() + @draw_y_axis() + redraw: -> + @set_domain() + @svg.select("path").datum(@dates) + @svg.select("path").attr("d", @area) + @svg.select(".x.axis").call(@x_axis) + @svg.select(".y.axis").call(@y_axis) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee new file mode 100644 index 00000000000..31617c88b4a --- /dev/null +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee @@ -0,0 +1,98 @@ +window.ContributorsStatGraphUtil = + parse_log: (log) -> + total = {} + by_author = {} + by_email = {} + for entry in log + @add_date(entry.date, total) unless total[entry.date]? + + data = by_author[entry.author_name] || by_email[entry.author_email] + data ?= @add_author(entry, by_author, by_email) + + @add_date(entry.date, data) unless data[entry.date] + @store_data(entry, total[entry.date], data[entry.date]) + total = _.toArray(total) + by_author = _.toArray(by_author) + total: total, by_author: by_author + + add_date: (date, collection) -> + collection[date] = {} + collection[date].date = date + + add_author: (author, by_author, by_email) -> + data = {} + data.author_name = author.author_name + data.author_email = author.author_email + by_author[author.author_name] = data + by_email[author.author_email] = data + + store_data: (entry, total, by_author) -> + @store_commits(total, by_author) + @store_additions(entry, total, by_author) + @store_deletions(entry, total, by_author) + + store_commits: (total, by_author) -> + @add(total, "commits", 1) + @add(by_author, "commits", 1) + + add: (collection, field, value) -> + collection[field] ?= 0 + collection[field] += value + + store_additions: (entry, total, by_author) -> + entry.additions ?= 0 + @add(total, "additions", entry.additions) + @add(by_author, "additions", entry.additions) + + store_deletions: (entry, total, by_author) -> + entry.deletions ?= 0 + @add(total, "deletions", entry.deletions) + @add(by_author, "deletions", entry.deletions) + + get_total_data: (parsed_log, field) -> + log = parsed_log.total + total_data = @pick_field(log, field) + _.sortBy(total_data, (d) -> + d.date + ) + pick_field: (log, field) -> + total_data = [] + _.each(log, (d) -> + total_data.push(_.pick(d, [field, 'date'])) + ) + total_data + + get_author_data: (parsed_log, field, date_range = null) -> + log = parsed_log.by_author + author_data = [] + + _.each(log, (log_entry) => + parsed_log_entry = @parse_log_entry(log_entry, field, date_range) + if not _.isEmpty(parsed_log_entry.dates) + author_data.push(parsed_log_entry) + ) + + _.sortBy(author_data, (d) -> + d[field] + ).reverse() + + parse_log_entry: (log_entry, field, date_range) -> + parsed_entry = {} + parsed_entry.author_name = log_entry.author_name + parsed_entry.author_email = log_entry.author_email + parsed_entry.dates = {} + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0 + _.each(_.omit(log_entry, 'author_name', 'author_email'), (value, key) => + if @in_range(value.date, date_range) + parsed_entry.dates[value.date] = value[field] + parsed_entry.commits += value.commits + parsed_entry.additions += value.additions + parsed_entry.deletions += value.deletions + ) + return parsed_entry + + in_range: (date, date_range) -> + if date_range is null || date_range[0] <= new Date(date) <= date_range[1] + true + else + false diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index afffed63ac5..6504e481102 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -1,7 +1,11 @@ +issuable_created = false @Issuable = init: -> - Issuable.initTemplates() - Issuable.initSearch() + unless issuable_created + issuable_created = true + Issuable.initTemplates() + Issuable.initSearch() + Issuable.initChecks() initTemplates: -> Issuable.labelRow = _.template( @@ -19,7 +23,16 @@ .on 'keyup', -> clearTimeout(@timer) @timer = setTimeout( -> - Issuable.filterResults $('#issue_search_form') + $search = $('#issue_search') + $form = $('.js-filter-form') + $input = $("input[name='#{$search.attr('name')}']", $form) + + if $input.length is 0 + $form.append "" + else + $input.val $search.val() + + Issuable.filterResults $form , 500) toggleLabelFilters: -> @@ -59,15 +72,22 @@ dataType: "json" reload: -> - if Issues.created - Issues.initChecks() + if Issuable.created + Issuable.initChecks() $('#filter_issue_search').val($('#issue_search').val()) + initChecks: -> + $('.check_all_issues').on 'click', -> + $('.selected_issue').prop('checked', @checked) + Issuable.checkChanged() + + $('.selected_issue').on 'change', Issuable.checkChanged + updateStateFilters: -> - stateFilters = $('.issues-state-filters') + stateFilters = $('.issues-state-filters, .dropdown-menu-sort') newParams = {} - paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search'] + paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search'] for paramKey in paramKeys newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' @@ -82,3 +102,17 @@ else newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) $(this).attr 'href', newUrl + + checkChanged: -> + checked_issues = $('.selected_issue:checked') + if checked_issues.length > 0 + ids = $.map checked_issues, (value) -> + $(value).data('id') + + $('#update_issues_ids').val ids + $('.issues-other-filters').hide() + $('.issues_bulk_update').show() + else + $('#update_issues_ids').val [] + $('.issues_bulk_update').hide() + $('.issues-other-filters').show() diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 7a788f761b7..898506fde32 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -19,6 +19,16 @@ class @IssuableForm @form.on "click", ".btn-cancel", @resetAutosave @initWip() + @initMoveDropdown() + + $issuableDueDate = $('#issuable-due-date') + + if $issuableDueDate.length + $('.datepicker').datepicker( + dateFormat: 'yy-mm-dd', + onSelect: (dateText, inst) -> + $issuableDueDate.val dateText + ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()) initAutosave: -> new Autosave @titleField, [ @@ -80,3 +90,19 @@ class @IssuableForm addWip: -> @titleField.val "WIP: #{@titleField.val()}" + + initMoveDropdown: -> + $moveDropdown = $('.js-move-dropdown') + + if $moveDropdown.length + $('.js-move-dropdown').select2 + ajax: + url: $moveDropdown.data('projects-url') + results: (data) -> + return { + results: data + } + formatResult: (project) -> + project.name_with_namespace + formatSelection: (project) -> + project.name_with_namespace diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee deleted file mode 100644 index 3330e6c68ad..00000000000 --- a/app/assets/javascripts/issues.js.coffee +++ /dev/null @@ -1,38 +0,0 @@ -@Issues = - init: -> - Issues.created = true - Issues.initChecks() - - $("body").on "ajax:success", ".close_issue, .reopen_issue", -> - t = $(this) - totalIssues = undefined - reopen = t.hasClass("reopen_issue") - $(".issue_counter").each -> - issue = $(this) - totalIssues = parseInt($(this).html(), 10) - if reopen and issue.closest(".main_menu").length - $(this).html totalIssues + 1 - else - $(this).html totalIssues - 1 - - initChecks: -> - $(".check_all_issues").click -> - $(".selected_issue").prop("checked", @checked) - Issues.checkChanged() - - $(".selected_issue").bind "change", Issues.checkChanged - - checkChanged: -> - checked_issues = $(".selected_issue:checked") - if checked_issues.length > 0 - ids = [] - $.each checked_issues, (index, value) -> - ids.push $(value).attr("data-id") - - $("#update_issues_ids").val ids - $(".issues-other-filters").hide() - $(".issues_bulk_update").show() - else - $("#update_issues_ids").val [] - $(".issues_bulk_update").hide() - $(".issues-other-filters").show() diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee new file mode 100644 index 00000000000..6adac6dac97 --- /dev/null +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -0,0 +1,14 @@ +class @LayoutNav + $ -> + $('.fade-left').addClass('end-scroll') + $('.scrolling-tabs').on 'scroll', (event) -> + $this = $(this) + $el = $(event.target) + currentPosition = $this.scrollLeft() + size = bp.getBreakpointSize() + controlBtnWidth = $('.controls').width() + maxPosition = $this.get(0).scrollWidth - $this.parent().width() + maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length + + $el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) + $el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/javascripts/lib/type_utility.js.coffee b/app/assets/javascripts/lib/type_utility.js.coffee new file mode 100644 index 00000000000..957f0d86b36 --- /dev/null +++ b/app/assets/javascripts/lib/type_utility.js.coffee @@ -0,0 +1,9 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.isObject = (obj) -> + obj? and (obj.constructor is Object) + +) window diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee index 6a00932c028..e8085e1c2e4 100644 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -26,10 +26,19 @@ newUrl = decodeURIComponent(url) for paramName, paramValue of params pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" - if url.search(pattern) >= 0 + if not paramValue? + newUrl = newUrl.replace pattern, '' + else if url.search(pattern) isnt -1 newUrl = newUrl.replace pattern, "$1#{paramValue}$2" else newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" + + # Remove a trailing ampersand + lastChar = newUrl[newUrl.length - 1] + + if lastChar is '&' + newUrl = newUrl.slice 0, -1 + newUrl # removes parameter query string from url. returns the modified url diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 372732d0aac..49a4727205a 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -75,6 +75,9 @@ class @MergeRequestTabs @loadDiff($target.attr('href')) if bp? and bp.getBreakpointSize() isnt 'lg' @shrinkView() + + navBarHeight = $('.navbar-gitlab').outerHeight() + $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) else if action == 'builds' @loadBuilds($target.attr('href')) @expandView() diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index f58647988a2..779f536d9f0 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -10,6 +10,7 @@ class @MergeRequestWidget $('#modal_merge_info').modal(show: false) @firstCICheck = true @readyForCICheck = false + @cancel = false clearInterval @fetchBuildStatusInterval @clearEventListeners() @@ -21,10 +22,16 @@ class @MergeRequestWidget clearEventListeners: -> $(document).off 'page:change.merge_request' + cancelPolling: -> + @cancel = true + addEventListeners: -> + allowedPages = ['show', 'commits', 'builds', 'changes'] $(document).on 'page:change.merge_request', => - if $('body').data('page') isnt 'projects:merge_requests:show' + page = $('body').data('page').split(':').last() + if allowedPages.indexOf(page) < 0 clearInterval @fetchBuildStatusInterval + @cancelPolling() @clearEventListeners() mergeInProgress: (deleteSourceBranch = false)-> @@ -67,6 +74,7 @@ class @MergeRequestWidget $('.ci-widget-fetching').show() $.getJSON @opts.ci_status_url, (data) => + return if @cancel @readyForCICheck = true if data.status is '' @@ -106,6 +114,7 @@ class @MergeRequestWidget @firstCICheck = false showCIStatus: (state) -> + return if not state? $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -113,7 +122,7 @@ class @MergeRequestWidget switch state when "failed", "canceled", "not_found" @setMergeButtonClass('btn-danger') - when "running", "pending" + when "running" @setMergeButtonClass('btn-warning') when "success" @setMergeButtonClass('btn-create') @@ -126,6 +135,6 @@ class @MergeRequestWidget $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> - $('.accept_merge_request') + $('.js-merge-button,.accept-action .dropdown-toggle') .removeClass('btn-danger btn-warning btn-create') .addClass(css_class) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index efb3e8e2198..f8151963fa7 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -114,9 +114,9 @@ class @Notes @refresh() , @pollingInterval - refresh: -> + refresh: => return if @refreshing is true - refreshing = true + @refreshing = true if not document.hidden and document.URL.indexOf(@noteable_url) is 0 @getContent() @@ -134,8 +134,8 @@ class @Notes @renderDiscussionNote(note) else @renderNote(note) - always: => - @refreshing = false + .always () => + @refreshing = false ### Increase @pollingInterval up to 120 seconds on every function call, @@ -285,6 +285,7 @@ class @Notes form.addClass "js-main-target-form" form.find("#note_line_code").remove() + form.find("#note_type").remove() ### General note form setup. @@ -328,7 +329,7 @@ class @Notes @renderDiscussionNote(note) # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}")) + @removeDiscussionNoteForm($(xhr.target)) ### Called in response to the edit note form being submitted @@ -472,6 +473,7 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" + form.find("#note_type").val dataHolder.data("noteType") form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee index 2d084b76cfe..c9cb0f4bb32 100644 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -10,6 +10,40 @@ class @Sidebar $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) + + $(document) + .off 'click', '.js-sidebar-toggle' + .on 'click', '.js-sidebar-toggle', (e, triggered) -> + e.preventDefault() + $this = $(this) + $thisIcon = $this.find 'i' + $allGutterToggleIcons = $('.js-sidebar-toggle i') + if $thisIcon.hasClass('fa-angle-double-right') + $allGutterToggleIcons + .removeClass('fa-angle-double-right') + .addClass('fa-angle-double-left') + $('aside.right-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed') + $('.page-with-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed') + else + $allGutterToggleIcons + .removeClass('fa-angle-double-left') + .addClass('fa-angle-double-right') + $('aside.right-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded') + $('.page-with-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded') + if not triggered + $.cookie("collapsed_gutter", + $('.right-sidebar') + .hasClass('right-sidebar-collapsed'), { path: '/' }) + + sidebarDropdownLoading: (e) -> $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') img = $sidebarCollapsedIcon.find('img') @@ -76,7 +110,7 @@ class @Sidebar @triggerOpenSidebar() if not @isOpen() if action is 'hide' - @triggerOpenSidebar() is @isOpen() + @triggerOpenSidebar() if @isOpen() isOpen: -> @sidebar.is('.right-sidebar-expanded') diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 6a7b4ad1db7..2122e80f57a 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -20,8 +20,7 @@ class @SearchAutocomplete @dropdown = @wrap.find('.dropdown') @dropdownContent = @dropdown.find('.dropdown-content') - @locationBadgeEl = @getElement('.search-location-badge') - @locationText = @getElement('.location-text') + @locationBadgeEl = @getElement('.location-badge') @scopeInputEl = @getElement('#scope') @searchInput = @getElement('.search-input') @projectInputEl = @getElement('#search_project_id') @@ -133,7 +132,7 @@ class @SearchAutocomplete scope: @scopeInputEl.val() # Location badge - _location: @locationText.text() + _location: @locationBadgeEl.text() } bindEvents: -> @@ -143,12 +142,14 @@ class @SearchAutocomplete @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus @clearInput.on 'click', @onClearInputClick + @locationBadgeEl.on 'click', => + @searchInput.focus() onDocumentClick: (e) => # If clicking outside the search box # And search input is not focused # And we are not clicking inside a suggestion - if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length @onSearchInputBlur() enableAutocomplete: -> @@ -221,10 +222,8 @@ class @SearchAutocomplete category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = " - #{category}#{value} - " - @locationBadgeEl.html(html) + badgeText = "#{category}#{value}" + @locationBadgeEl.text(badgeText).show() @wrap.addClass('has-location-badge') restoreOriginalState: -> @@ -233,9 +232,8 @@ class @SearchAutocomplete for input in inputs @getElement("##{input}").val(@originalState[input]) - if @originalState._location is '' - @locationBadgeEl.empty() + @locationBadgeEl.hide() else @addLocationBadge( value: @originalState._location @@ -244,7 +242,7 @@ class @SearchAutocomplete @dropdown.removeClass 'open' badgePresent: -> - @locationBadgeEl.children().length + @locationBadgeEl.length resetSearchState: -> inputs = Object.keys @originalState @@ -257,7 +255,7 @@ class @SearchAutocomplete @getElement("##{input}").val('') removeLocationBadge: -> - @locationBadgeEl.empty() + @locationBadgeEl.hide() # Reset state @resetSearchState() diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee index 4a05bdccdb3..cca2b8a1fcc 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee @@ -3,10 +3,10 @@ class @ShortcutsDashboardNavigation extends Shortcuts constructor: -> super() - Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-activity')) - Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-merge_requests')) - Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-projects')) + Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity')) + Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues')) + Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests')) + Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects')) @findAndFollowLink: (selector) -> link = $(selector).attr('href') diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index ad9b3c1c6bf..ccb42ab2168 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -6,6 +6,10 @@ class @ShortcutsIssuable extends ShortcutsNavigation super() Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) + Mousetrap.bind('r', => + @replyWithSelectedText() + return false + ) Mousetrap.bind('j', => @prevIssue() return false diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 860d4f438d0..ea4ac52da31 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -12,7 +12,7 @@ toggleSidebar = -> niceScrollBars.updateScrollBar(); ), 300 -$(document).on("click", '.toggle-nav-collapse', (e) -> +$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> e.preventDefault() toggleSidebar() diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/stat_graph.js.coffee deleted file mode 100644 index f36c71fd25e..00000000000 --- a/app/assets/javascripts/stat_graph.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -class @StatGraph - @log: {} - @get_log: -> - @log - @set_log: (data) -> - @log = data diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee deleted file mode 100644 index 3be14cb43dd..00000000000 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ /dev/null @@ -1,72 +0,0 @@ -#= require d3 -#= require stat_graph_contributors_util - -class @ContributorsStatGraph - init: (log) -> - @parsed_log = ContributorsStatGraphUtil.parse_log(log) - @set_current_field("commits") - total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) - author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field) - @add_master_graph(total_commits) - @add_authors_graph(author_commits) - @change_date_header() - add_master_graph: (total_data) -> - @master_graph = new ContributorsMasterGraph(total_data) - @master_graph.draw() - add_authors_graph: (author_data) -> - @authors = [] - limited_author_data = author_data.slice(0, 100) - _.each(limited_author_data, (d) => - author_header = @create_author_header(d) - $(".contributors-list").append(author_header) - @authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates) - author_graph.draw() - ) - format_author_commit_info: (author) -> - commits = $('', { - class: 'graph-author-commits-count' - }) - commits.text(author.commits + " commits") - $('').append(commits) - - create_author_header: (author) -> - list_item = $('
  • ', { - class: 'person' - style: 'display: block;' - }) - author_name = $('

    ' + author.author_name + '

    ') - author_email = $('

    ' + author.author_email + '

    ') - author_commit_info_span = $('', { - class: 'commits' - }) - author_commit_info = @format_author_commit_info(author) - author_commit_info_span.html(author_commit_info) - list_item.append(author_name) - list_item.append(author_email) - list_item.append(author_commit_info_span) - list_item - redraw_master: -> - total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) - @master_graph.set_data(total_data) - @master_graph.redraw() - redraw_authors: -> - $("ol").html("") - x_domain = ContributorsGraph.prototype.x_domain - author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain) - _.each(author_commits, (d) => - @redraw_author_commit_info(d) - $(@authors[d.author_name].list_item).appendTo("ol") - @authors[d.author_name].set_data(d.dates) - @authors[d.author_name].redraw() - ) - set_current_field: (field) -> - @field = field - change_date_header: -> - x_domain = ContributorsGraph.prototype.x_domain - print_date_format = d3.time.format("%B %e %Y") - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]) - $("#date_header").text(print) - redraw_author_commit_info: (author) -> - author_list_item = $(@authors[author.author_name].list_item) - author_commit_info = @format_author_commit_info(author) - author_list_item.find("span").html(author_commit_info) diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee deleted file mode 100644 index b7a0e073766..00000000000 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ /dev/null @@ -1,171 +0,0 @@ -#= require d3 -#= require jquery -#= require underscore - -class @ContributorsGraph - MARGIN: - top: 20 - right: 20 - bottom: 30 - left: 50 - x_domain: null - y_domain: null - dates: [] - @set_x_domain: (data) => - @prototype.x_domain = data - @set_y_domain: (data) => - @prototype.y_domain = [0, d3.max(data, (d) -> - d.commits = d.commits ? d.additions ? d.deletions - )] - @init_x_domain: (data) => - @prototype.x_domain = d3.extent(data, (d) -> - d.date - ) - @init_y_domain: (data) => - @prototype.y_domain = [0, d3.max(data, (d) -> - d.commits = d.commits ? d.additions ? d.deletions - )] - @init_domain: (data) => - @init_x_domain(data) - @init_y_domain(data) - @set_dates: (data) => - @prototype.dates = data - set_x_domain: -> - @x.domain(@x_domain) - set_y_domain: -> - @y.domain(@y_domain) - set_domain: -> - @set_x_domain() - @set_y_domain() - create_scale: (width, height) -> - @x = d3.time.scale().range([0, width]).clamp(true) - @y = d3.scale.linear().range([height, 0]).nice() - draw_x_axis: -> - @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})") - .call(@x_axis) - draw_y_axis: -> - @svg.append("g").attr("class", "y axis").call(@y_axis) - set_data: (data) -> - @data = data - -class @ContributorsMasterGraph extends ContributorsGraph - constructor: (@data) -> - @width = $('.content').width() - 70 - @height = 200 - @x = null - @y = null - @x_axis = null - @y_axis = null - @area = null - @svg = null - @brush = null - @x_max_domain = null - process_dates: (data) -> - dates = @get_dates(data) - @parse_dates(data) - ContributorsGraph.set_dates(dates) - get_dates: (data) -> - _.pluck(data, 'date') - parse_dates: (data) -> - parseDate = d3.time.format("%Y-%m-%d").parse - data.forEach((d) -> - d.date = parseDate(d.date) - ) - create_scale: -> - super @width, @height - create_axes: -> - @x_axis = d3.svg.axis().scale(@x).orient("bottom") - @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) - create_svg: -> - @svg = d3.select("#contributors-master").append("svg") - .attr("width", @width + @MARGIN.left + @MARGIN.right) - .attr("height", @height + @MARGIN.top + @MARGIN.bottom) - .attr("class", "tint-box") - .append("g") - .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") - create_area: (x, y) -> - @area = d3.svg.area().x((d) -> - x(d.date) - ).y0(@height).y1((d) -> - xa = d.commits = d.commits ? d.additions ? d.deletions - y(xa) - ).interpolate("basis") - create_brush: -> - @brush = d3.svg.brush().x(@x).on("brushend", @update_content) - draw_path: (data) -> - @svg.append("path").datum(data).attr("class", "area").attr("d", @area) - add_brush: -> - @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height) - update_content: => - ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent()) - $("#brush_change").trigger('change') - draw: -> - @process_dates(@data) - @create_scale() - @create_axes() - ContributorsGraph.init_domain(@data) - @x_max_domain = @x_domain - @set_domain() - @create_area(@x, @y) - @create_svg() - @create_brush() - @draw_path(@data) - @draw_x_axis() - @draw_y_axis() - @add_brush() - redraw: -> - @process_dates(@data) - ContributorsGraph.set_y_domain(@data) - @set_y_domain() - @svg.select("path").datum(@data) - @svg.select("path").attr("d", @area) - @svg.select(".y.axis").call(@y_axis) - -class @ContributorsAuthorGraph extends ContributorsGraph - constructor: (@data) -> - @width = $('.content').width()/2 - 100 - @height = 200 - @x = null - @y = null - @x_axis = null - @y_axis = null - @area = null - @svg = null - @list_item = null - create_scale: -> - super @width, @height - create_axes: -> - @x_axis = d3.svg.axis().scale(@x).orient("bottom").ticks(8) - @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5) - create_area: (x, y) -> - @area = d3.svg.area().x((d) -> - parseDate = d3.time.format("%Y-%m-%d").parse - x(parseDate(d)) - ).y0(@height).y1((d) => - if @data[d]? then y(@data[d]) else y(0) - ).interpolate("basis") - create_svg: -> - @list_item = d3.selectAll(".person")[0].pop() - @svg = d3.select(@list_item).append("svg") - .attr("width", @width + @MARGIN.left + @MARGIN.right) - .attr("height", @height + @MARGIN.top + @MARGIN.bottom) - .attr("class", "spark") - .append("g") - .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") - draw_path: (data) -> - @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area) - draw: -> - @create_scale() - @create_axes() - @set_domain() - @create_area(@x, @y) - @create_svg() - @draw_path(@dates) - @draw_x_axis() - @draw_y_axis() - redraw: -> - @set_domain() - @svg.select("path").datum(@dates) - @svg.select("path").attr("d", @area) - @svg.select(".x.axis").call(@x_axis) - @svg.select(".y.axis").call(@y_axis) diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee deleted file mode 100644 index 31617c88b4a..00000000000 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ /dev/null @@ -1,98 +0,0 @@ -window.ContributorsStatGraphUtil = - parse_log: (log) -> - total = {} - by_author = {} - by_email = {} - for entry in log - @add_date(entry.date, total) unless total[entry.date]? - - data = by_author[entry.author_name] || by_email[entry.author_email] - data ?= @add_author(entry, by_author, by_email) - - @add_date(entry.date, data) unless data[entry.date] - @store_data(entry, total[entry.date], data[entry.date]) - total = _.toArray(total) - by_author = _.toArray(by_author) - total: total, by_author: by_author - - add_date: (date, collection) -> - collection[date] = {} - collection[date].date = date - - add_author: (author, by_author, by_email) -> - data = {} - data.author_name = author.author_name - data.author_email = author.author_email - by_author[author.author_name] = data - by_email[author.author_email] = data - - store_data: (entry, total, by_author) -> - @store_commits(total, by_author) - @store_additions(entry, total, by_author) - @store_deletions(entry, total, by_author) - - store_commits: (total, by_author) -> - @add(total, "commits", 1) - @add(by_author, "commits", 1) - - add: (collection, field, value) -> - collection[field] ?= 0 - collection[field] += value - - store_additions: (entry, total, by_author) -> - entry.additions ?= 0 - @add(total, "additions", entry.additions) - @add(by_author, "additions", entry.additions) - - store_deletions: (entry, total, by_author) -> - entry.deletions ?= 0 - @add(total, "deletions", entry.deletions) - @add(by_author, "deletions", entry.deletions) - - get_total_data: (parsed_log, field) -> - log = parsed_log.total - total_data = @pick_field(log, field) - _.sortBy(total_data, (d) -> - d.date - ) - pick_field: (log, field) -> - total_data = [] - _.each(log, (d) -> - total_data.push(_.pick(d, [field, 'date'])) - ) - total_data - - get_author_data: (parsed_log, field, date_range = null) -> - log = parsed_log.by_author - author_data = [] - - _.each(log, (log_entry) => - parsed_log_entry = @parse_log_entry(log_entry, field, date_range) - if not _.isEmpty(parsed_log_entry.dates) - author_data.push(parsed_log_entry) - ) - - _.sortBy(author_data, (d) -> - d[field] - ).reverse() - - parse_log_entry: (log_entry, field, date_range) -> - parsed_entry = {} - parsed_entry.author_name = log_entry.author_name - parsed_entry.author_email = log_entry.author_email - parsed_entry.dates = {} - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0 - _.each(_.omit(log_entry, 'author_name', 'author_email'), (value, key) => - if @in_range(value.date, date_range) - parsed_entry.dates[value.date] = value[field] - parsed_entry.commits += value.commits - parsed_entry.additions += value.additions - parsed_entry.deletions += value.deletions - ) - return parsed_entry - - in_range: (date, date_range) -> - if date_range is null || date_range[0] <= new Date(date) <= date_range[1] - true - else - false diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index c2aeffe2381..70614396a4e 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -26,6 +26,10 @@ # Personal projects # #
  • +#
  • +# +# +#
  • # # #
    @@ -41,6 +45,9 @@ #
    # Projects content #
    +#
    +# Snippets content +#
    #
    # #
    @@ -100,7 +107,7 @@ class @UserTabs if action is 'activity' @loadActivities(source) - if action in ['groups', 'contributed', 'projects'] + if action in ['groups', 'contributed', 'projects', 'snippets'] @loadTab(source, action) loadTab: (source, action) -> diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee new file mode 100644 index 00000000000..647ffbf5f45 --- /dev/null +++ b/app/assets/javascripts/users/application.js.coffee @@ -0,0 +1,8 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require d3 +#= require_tree . diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee new file mode 100644 index 00000000000..26a26061539 --- /dev/null +++ b/app/assets/javascripts/users/calendar.js.coffee @@ -0,0 +1,198 @@ +class @Calendar + constructor: (timestamps, @calendar_activities_path) -> + @currentSelectedDate = '' + @daySpace = 1 + @daySize = 15 + @daySizeWithSpace = @daySize + (@daySpace * 2) + @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + @months = [] + @highestValue = 0 + + # Get the highest value from the timestampes + _.each timestamps, (count) => + if count > @highestValue + @highestValue = count + + # Loop through the timestamps to create a group of objects + # The group of objects will be grouped based on the day of the week they are + @timestampsTmp = [] + i = 0 + group = 0 + _.each timestamps, (count, date) => + newDate = new Date parseInt(date) * 1000 + day = newDate.getDay() + + # Create a new group array if this is the first day of the week + # or if is first object + if (day is 0 and i isnt 0) or i is 0 + @timestampsTmp.push [] + group++ + + innerArray = @timestampsTmp[group-1] + + # Push to the inner array the values that will be used to render map + innerArray.push + count: count + date: newDate + day: day + + i++ + + # Init color functions + @color = @initColor() + @colorKey = @initColorKey() + + # Init the svg element + @renderSvg(group) + @renderDays() + @renderMonths() + @renderDayTitles() + @renderKey() + + @initTooltips() + + renderSvg: (group) -> + @svg = d3.select '.js-contrib-calendar' + .append 'svg' + .attr 'width', (group + 1) * @daySizeWithSpace + .attr 'height', 167 + .attr 'class', 'contrib-calendar' + + renderDays: -> + @svg.selectAll 'g' + .data @timestampsTmp + .enter() + .append 'g' + .attr 'transform', (group, i) => + _.each group, (stamp, a) => + if a is 0 and stamp.day is 0 + month = stamp.date.getMonth() + x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace + lastMonth = _.last(@months) + if lastMonth? + lastMonthX = lastMonth.x + + if !lastMonth? + @months.push + month: month + x: x + else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX + @months.push + month: month + x: x + + "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)" + .selectAll 'rect' + .data (stamp) -> + stamp + .enter() + .append 'rect' + .attr 'x', '0' + .attr 'y', (stamp, i) => + (@daySizeWithSpace * stamp.day) + .attr 'width', @daySize + .attr 'height', @daySize + .attr 'title', (stamp) => + contribText = 'No contributions' + + if stamp.count > 0 + contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}" + + date = dateFormat(stamp.date, 'mmm d, yyyy') + + "#{contribText}
    #{date}" + .attr 'class', 'user-contrib-cell js-tooltip' + .attr 'fill', (stamp) => + if stamp.count isnt 0 + @color(stamp.count) + else + '#ededed' + .attr 'data-container', 'body' + .on 'click', @clickDay + + renderDayTitles: -> + days = [{ + text: 'M' + y: 29 + (@daySizeWithSpace * 1) + }, { + text: 'W' + y: 29 + (@daySizeWithSpace * 3) + }, { + text: 'F' + y: 29 + (@daySizeWithSpace * 5) + }] + @svg.append 'g' + .selectAll 'text' + .data days + .enter() + .append 'text' + .attr 'text-anchor', 'middle' + .attr 'x', 8 + .attr 'y', (day) -> + day.y + .text (day) -> + day.text + .attr 'class', 'user-contrib-text' + + renderMonths: -> + @svg.append 'g' + .selectAll 'text' + .data @months + .enter() + .append 'text' + .attr 'x', (date) -> + date.x + .attr 'y', 10 + .attr 'class', 'user-contrib-text' + .text (date) => + @monthNames[date.month] + + renderKey: -> + keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] + @svg.append 'g' + .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})" + .selectAll 'rect' + .data keyColors + .enter() + .append 'rect' + .attr 'width', @daySize + .attr 'height', @daySize + .attr 'x', (color, i) => + @daySizeWithSpace * i + .attr 'y', 0 + .attr 'fill', (color) -> + color + + initColor: -> + d3.scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, @highestValue]) + + initColorKey: -> + d3.scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, 3]) + + clickDay: (stamp) => + if @currentSelectedDate isnt stamp.date + @currentSelectedDate = stamp.date + formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate() + + $.ajax + url: @calendar_activities_path + data: + date: formatted_date + cache: false + dataType: 'html' + beforeSend: -> + $('.user-calendar-activities').html '
    ' + success: (data) -> + $('.user-calendar-activities').html data + else + $('.user-calendar-activities').html '' + + initTooltips: -> + $('.js-contrib-calendar .js-tooltip').tooltip + html: true diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index b80b1b861cc..519618aa617 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -93,7 +93,9 @@ class @UsersSelect $dropdown.glDropdown( data: (term, callback) => - @users term, (users) => + isAuthorFilter = $('.js-author-search') + + @users term, term is '' and isAuthorFilter, (users) => if term.length is 0 showDivider = 0 @@ -138,7 +140,7 @@ class @UsersSelect toggleLabel: (selected) -> if selected && 'id' of selected - selected.name + if selected.text then selected.text else selected.name else defaultLabel @@ -219,7 +221,7 @@ class @UsersSelect multiple: $(select).hasClass('multiselect') minimumInputLength: 0 query: (query) => - @users query.term, (users) => + @users query.term, @projectId?, (users) => data = { results: users } if query.term.length == 0 @@ -302,7 +304,7 @@ class @UsersSelect # Return users list. Filtered by query # Only active users retrieved - users: (query, callback) => + users: (query, fromProject, callback) => url = @buildUrl(@usersPath) $.ajax( @@ -311,7 +313,7 @@ class @UsersSelect search: query per_page: 20 active: true - project_id: @projectId + project_id: @projectId if fromProject group_id: @groupId current_user: @showCurrentUser author_id: @authorId diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 69b3b6586de..8b93665d085 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -8,9 +8,7 @@ *= require select2 *= require_self *= require dropzone/basic - *= require cal-heatmap *= require cropper.css - *= require animate */ /* diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 560de9fc0bd..3cbddc59f11 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -5,6 +5,7 @@ @import 'framework/tw_bootstrap'; @import "framework/layout"; +@import "framework/animations.scss"; @import "framework/avatar.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss new file mode 100644 index 00000000000..1fec61bdba1 --- /dev/null +++ b/app/assets/stylesheets/framework/animations.scss @@ -0,0 +1,72 @@ +// This file is based off animate.css 3.5.1, available here: +// https://github.com/daneden/animate.css/blob/3.5.1/animate.css +// +// animate.css - http://daneden.me/animate +// Version - 3.5.1 +// Licensed under the MIT license - http://opensource.org/licenses/MIT +// +// Copyright (c) 2016 Daniel Eden + +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} + +.animated.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} + +.animated.flipOutX, +.animated.flipOutY, +.animated.bounceIn, +.animated.bounceOut { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} + +@-webkit-keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index f5ce70b606b..bb8d71fbae8 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -45,6 +45,7 @@ &.s32 { font-size: 20px; line-height: 32px; } &.s40 { font-size: 16px; line-height: 40px; } &.s60 { font-size: 32px; line-height: 60px; } + &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 90px; } &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 140px; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 434a26d57c6..6981f834d30 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -24,8 +24,8 @@ background-color: $background-color; padding: $gl-padding; margin-bottom: 0; - border-top: 1px solid $border-color; - border-bottom: 1px solid $border-color; + border-top: 1px solid $white-dark; + border-bottom: 1px solid $white-dark; color: $gl-gray; &.oneline-block { @@ -110,9 +110,9 @@ .cover-title { color: $gl-header-color; margin: 0; - font-size: 23px; + font-size: 24px; font-weight: normal; - margin: 16px 0 5px; + margin-bottom: 5px; color: #4c4e54; font-size: 23px; line-height: 1.1; @@ -137,7 +137,6 @@ } .cover-desc { - padding: 0 $gl-padding 3px; color: $gl-text-color; &.username:last-child { @@ -205,7 +204,7 @@ .content-block { padding: $gl-padding 0; - border-bottom: 1px solid $border-color; + border-bottom: 1px solid $white-dark; &.oneline-block { line-height: 36px; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index eaf85bb17ca..467f3b35d74 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -16,6 +16,19 @@ @include btn-default; } +@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) { + background-color: $background; + color: $text; + border-color: $border; + + &:hover, + &:focus { + background-color: $hover-background; + color: $hover-text; + border-color: $hover-border;; + } +} + @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -106,11 +119,14 @@ @include btn-blue; } - &.btn-close, &.btn-warning { @include btn-orange; } + &.btn-close { + @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); + } + &.btn-danger, &.btn-remove, &.btn-red { diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 11f39d583bd..8642b7530e2 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,70 +1,44 @@ .calender-block { + padding-left: 0; + padding-right: 0; + @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { overflow-x: scroll; } } .user-calendar-activities { - .calendar_onclick_hr { - padding: 0; - margin: 10px 0; - } - .str-truncated { max-width: 70%; } - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; - } + .user-calendar-activities-loading { + font-size: 24px; } } -/** -* This overwrites the default values of the cal-heatmap gem -*/ -.calendar { - .qi { - fill: #fff; - } - - .q1 { - fill: #ededed !important; - } +.user-calendar { + text-align: center; - .q2 { - fill: #acd5f2 !important; - } - - .q3 { - fill: #7fa8d1 !important; - } - - .q4 { - fill: #49729b !important; - } - - .q5 { - fill: #254e77 !important; + .calendar { + display: inline-block; } +} - .future { - visibility: hidden; +.user-contrib-cell { + &:hover { + cursor: pointer; + stroke: #000; } +} - .domain-background { - fill: none; - shape-rendering: crispedges; - } +.user-contrib-text { + font-size: 12px; + fill: #959494; +} - .ch-tooltip { - padding: 3px; - font-weight: 550; - } +.calendar-hint { + margin-top: -23px; + float: right; + font-size: 12px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3386523dbf7..f8aecd0558d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -289,7 +289,7 @@ table { text-shadow: none; @media (min-width: $screen-sm-min) { - margin-top: 11px; + margin-top: 8px; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 4bf3a050403..93c63c69843 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -154,7 +154,7 @@ color: $dropdown-header-color; font-size: 13px; line-height: 22px; - padding: 0 10px 10px; + padding: 0 10px; } .separator + .dropdown-header { @@ -162,6 +162,10 @@ } } +.dropdown-menu-full-width { + width: 100%; +} + .dropdown-menu-paging { .dropdown-page-two, .dropdown-menu-back { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 61d9954c6c8..71a9f79be3e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -5,6 +5,10 @@ .file-holder { border: 1px solid $border-color; + &.file-holder-no-border { + border: 0; + } + &.readme-holder { margin: $gl-padding-top 0; } @@ -23,8 +27,17 @@ word-wrap: break-word; border-radius: 3px 3px 0 0; + &.file-title-clear { + padding-left: 0; + padding-right: 0; + background-color: transparent; + + .file-actions { + right: 0; + } + } + .file-actions { - float: right; position: absolute; top: 5px; right: 15px; @@ -36,22 +49,6 @@ } } - .filename { - &.old { - display: inline-block; - span.idiff { - background-color: #f8cbcb; - } - } - - &.new { - display: inline-block; - span.idiff { - background-color: #a6f3a6; - } - } - } - a:not(.btn) { color: $gl-dark-link-color; } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 558b133f593..46acc3b772f 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -28,10 +28,6 @@ input[type='text'].danger { } label { - &.control-label { - @extend .col-sm-2; - } - &.inline-label { margin: 0; } @@ -41,6 +37,10 @@ label { } } +.control-label { + @extend .col-sm-2; +} + .inline-input-group { width: 250px; } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c83cf881596..16cf394c426 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -9,8 +9,7 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { .header-logo { - background-color: $color; - border-color: $color; + background: $color-darker; a { color: $color-light; @@ -21,9 +20,13 @@ } &:hover { - background-color: $color-darker; + background-color: $color-dark; a { color: #fff; + + h3 { + color: #fff; + } } } } @@ -87,8 +90,8 @@ } $theme-blue: #2980b9; -$theme-charcoal: #333c47; -$theme-graphite: #888; +$theme-charcoal: #3d454d; +$theme-graphite: #666; $theme-gray: #373737; $theme-green: #019875; $theme-violet: #548; @@ -99,11 +102,11 @@ body { } &.ui_charcoal { - @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d); + @include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41); } &.ui_graphite { - @include gitlab-theme(#ccc, $theme-graphite, #777, #666); + @include gitlab-theme(#ccc, #777, $theme-graphite, #555); } &.ui_gray { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 97f9d582007..0da96c4017d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -6,12 +6,12 @@ header { transition-duration: .3s; &.navbar-empty { - height: 58px; + height: $header-height; background: #fff; - border-bottom: 1px solid #eee; + border-bottom: 1px solid $btn-gray-hover; .center-logo { - margin: 11px 0; + margin: 8px 0; text-align: center; #tanuki-logo, img { @@ -22,14 +22,18 @@ header { } &.navbar-gitlab { - padding: 0 20px; + padding: 0 16px; z-index: 100; margin-bottom: 0; - min-height: $header-height; + height: $header-height; background-color: $background-color; border: none; border-bottom: 1px solid $border-color; + @media (max-width: $screen-xs-min) { + padding: 0 16px; + } + &.with-horizontal-nav { border-bottom: none; } @@ -60,16 +64,44 @@ header { margin: 6px 0; border-radius: 0; position: absolute; - right: 2px; + right: -10px; + padding: 6px 10px; &:hover { - background-color: #eee; + background-color: $btn-gray-hover; } + &.active { color: $gl-icon-color; } } } + + &.header-collapsed { + padding: 0 16px; + } + + .side-nav-toggle { + display: none; + position: absolute; + left: -10px; + margin: 6px 0; + padding: 6px 10px; + border: none; + background-color: $background-color; + + &:hover { + background-color: $btn-gray-hover; + } + + &:focus { + outline: none; + } + + @media (max-width: $screen-xs-min) { + display: block; + } + } } .header-content { @@ -77,6 +109,10 @@ header { height: $header-height; padding-right: 40px; + @media (max-width: $screen-xs-min) { + padding-left: 40px; + } + @media (min-width: $screen-sm-min) { padding-right: 0; } @@ -145,6 +181,10 @@ header { @media (min-width: $screen-md-min) { @include collapsed-header; } + + @media (max-width: $screen-xs-min) { + margin-left: 0; + } } .header-expanded { @@ -153,6 +193,10 @@ header { @media (min-width: $screen-md-min) { margin-left: $sidebar_width; } + + @media (max-width: $screen-xs-min) { + margin-left: 0; + } } @media (max-width: $screen-xs-max) { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 33cbee85987..bd531f8376b 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -48,10 +48,6 @@ display: block; } - .project-home-desc { - font-size: 21px; - } - .project-repo-buttons, .git-clone-holder { display: none; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7c18e93a261..7eb7a8e4544 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,3 +1,34 @@ +@mixin fade($gradient-direction, $rgba, $gradient-color) { + visibility: visible; + opacity: 1; + position: absolute; + bottom: 12px; + width: 43px; + height: 30px; + transition-duration: .3s; + -webkit-transform: translateZ(0); + background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + + &.end-scroll { + visibility: hidden; + opacity: 0; + transition-duration: .3s; + } +} + +@mixin scrolling-links() { + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + &::-webkit-scrollbar { + display: none; + } +} + .nav-links { padding: 0; margin: 0; @@ -119,7 +150,7 @@ } input { - height: 34px; + height: 35px; display: inline-block; position: relative; top: 2px; @@ -196,14 +227,22 @@ position: fixed; top: $header-height; width: 100%; - z-index: 1; + z-index: 11; background: $background-color; border-bottom: 1px solid $border-color; transition-duration: .3s; + .container-fluid { + position: relative; + } + .controls { float: right; - padding: 7px 5px 0 0; + padding: 7px 0 0; + + @media (max-width: $screen-xs-max) { + display: none; + } i { color: $layout-link-gray; @@ -221,14 +260,31 @@ .dropdown { margin-left: 7px; + + @media (max-width: $screen-xs-min) { + margin-left: 0; + } + + li.active { + font-weight: bold; + } } } .nav-links { + @include scrolling-links(); border-bottom: none; height: 51px; - white-space: nowrap; - overflow-x: auto; + + .fade-right { + @include fade(left, rgba(250, 250, 250, 0.4), $background-color); + right: 0; + } + + .fade-left { + @include fade(right, rgba(250, 250, 250, 0.4), $background-color); + left: 0; + } li { @@ -252,8 +308,62 @@ } } + .nav-control { + .fade-right { + + @media (min-width: $screen-xs-max) { + right: 67px; + } + @media (max-width: $screen-xs-min) { + right: 0; + } + } + } +} + +.nav-block { + position: relative; + + .nav-links { + @include scrolling-links(); + + .fade-right { + @include fade(left, rgba(255, 255, 255, 0.4), $white-light); + right: 0; + } + + .fade-left { + @include fade(right, rgba(255, 255, 255, 0.4), $white-light); + left: 0; + } + + &.event-filter { + .fade-right { + visibility: hidden; + + @media (max-width: $screen-xs-max) { + visibility: visible; + } + } + } + } } .page-with-layout-nav { - margin-top: 50px; + margin-top: $header-height + 2; + + .right-sidebar { + top: ($header-height * 2) + 2; + } +} + +.activities { + + .nav-block { + border-bottom: 1px solid $border-color; + + .nav-links { + border-bottom: none; + } + } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index e940fd7286e..67f491b6d9c 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -98,7 +98,7 @@ } .sidebar-user { - padding: 9px 22px; + padding: 7px 22px; position: fixed; bottom: 40px; width: $sidebar_width; @@ -210,15 +210,33 @@ } } +.sidebar-wrapper { + &.hidden-nav { + width: 0; + } +} + .page-sidebar-collapsed { padding-left: $sidebar_collapsed_width; + @media (max-width: $screen-xs-min) { + padding-left: 0; + } + .sidebar-wrapper { width: $sidebar_collapsed_width; + @media (max-width: $screen-xs-min) { + width: 0; + } + .header-logo { width: $sidebar_collapsed_width; + @media (max-width: $screen-xs-min) { + width: 0; + } + a { padding-left: ($sidebar_collapsed_width - 36) / 2; @@ -244,12 +262,22 @@ .collapse-nav a { width: $sidebar_collapsed_width; + + @media (max-width: $screen-xs-min) { + width: 0; + } } .sidebar-user { padding-left: ($sidebar_collapsed_width - 36) / 2; width: $sidebar_collapsed_width; + @media (max-width: $screen-xs-min) { + width: 0; + padding-left: 0; + padding-right: 0; + } + .username { display: none; } @@ -258,6 +286,10 @@ .layout-nav { padding-right: $sidebar_collapsed_width; + + @media (max-width: $screen-xs-min) { + padding-right: 0;; + } } } @@ -268,6 +300,10 @@ padding-left: $sidebar_width; } + @media (max-width: $screen-xs-min) { + padding-left: 0; + } + .sidebar-wrapper { width: $sidebar_width; @@ -276,7 +312,7 @@ } .nav-sidebar li a { - width: 230px; + width: $sidebar_width; &.back-link { i { @@ -287,7 +323,17 @@ } .layout-nav { - padding-right: $sidebar_width; + @media (max-width: $screen-xs-min) { + padding-right: 0; + } + + @media (min-width: $screen-xs-min) and (max-width: $screen-md-min) { + padding-right: 62px; + } + + @media (min-width: $screen-md-min) { + padding-right: $sidebar_width; + } } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 2779cd56788..3575984b229 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -269,3 +269,11 @@ h1, h2, h3, h4 { text-align: right; } } + +.idiff.deletion { + background: $line-removed-dark; +} + +.idiff.addition { + background: $line-added-dark; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ccb4e5381b7..f253da814bc 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -2,7 +2,7 @@ * Layout */ $sidebar_collapsed_width: 62px; -$sidebar_width: 230px; +$sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; @@ -12,7 +12,7 @@ $gutter_inner_width: 258px; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #ececec; +$table-border-color: #f0f0f0; $background-color: #fafafa; /* @@ -63,7 +63,8 @@ $gl-padding-top: 10px; /* * Misc */ -$row-hover: #f4f8fe; +$row-hover: #f7faff; +$row-hover-border: #b2d7ff; $progress-color: #c0392b; $avatar_radius: 50%; $header-height: 50px; @@ -104,7 +105,7 @@ $blue-medium-light: #3498cb; $blue-medium: #2f8ebf; $blue-medium-dark: #2d86b4; -$orange-light: rgba(252, 109, 38, 0.80); +$orange-light: #fc8a51; $orange-normal: #e75e40; $orange-dark: #ce5237; @@ -119,8 +120,8 @@ $border-white-light: #f1f2f4; $border-white-normal: #d6dae2; $border-white-dark: #c6cacf; -$border-gray-light: rgba(0, 0, 0, 0.06); -$border-gray-normal: rgba(0, 0, 0, 0.10);; +$border-gray-light: #dcdcdc; +$border-gray-normal: rgba(0, 0, 0, 0.10); $border-gray-dark: #c6cacf; $border-green-light: #2faa60; @@ -178,6 +179,7 @@ $table-border-gray: #f0f0f0; $line-target-blue: #eaf3fc; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; + /* * Fonts */ @@ -215,6 +217,7 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $btn-active-gray: #ececec; $btn-placeholder-gray: #c7c7c7; $btn-white-active: #848484; +$btn-gray-hover: #eee; /* * Award emoji diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index f870ea0d87f..ff02ebdd34c 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -32,7 +32,7 @@ } } -.zen-cotrol { +.zen-control { padding: 0; color: #555; background: none; diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss new file mode 100644 index 00000000000..28611a5ec81 --- /dev/null +++ b/app/assets/stylesheets/mailers/devise.scss @@ -0,0 +1,134 @@ +// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout +// used for Devise email templates, and _should not_ be included in any +// application stylesheets. +// +// Styles defined here are embedded directly into the resulting email HTML via +// the `premailer` gem. + +$body-background-color: #363636; +$message-background-color: #fafafa; + +$header-color: #6b4fbb; +$body-color: #444; +$cta-color: #e14329; +$footer-link-color: #7e7e7e; + +$font-family: Helvetica, Arial, sans-serif; + +body { + background-color: $body-background-color; + font-family: $font-family; + margin: 0; + padding: 0; +} + +table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + + border: 0; + border-collapse: separate; + + &#wrapper { + background-color: $body-background-color; + width: 100%; + } + + &#header { + margin: 0 auto; + text-align: left; + width: 600px; + } + + &#body { + background-color: $message-background-color; + border: 1px solid #000; + border-radius: 4px; + margin: 0 auto; + width: 600px; + } + + &#footer { + color: $footer-link-color; + font-size: 14px; + text-align: center; + width: 100%; + } + + td { + &#body-container { + padding: 20px 40px; + } + } +} + +.center { + text-align: center; +} + +#logo { + border: none; + outline: none; + min-height: 88px; + width: 134px; +} + +#content { + h2 { + color: $header-color; + font-size: 30px; + font-weight: 400; + line-height: 34px; + margin-top: 0; + } + + p { + color: $body-color; + font-size: 17px; + line-height: 24px; + margin-bottom: 0; + } +} + +#cta { + border: 1px solid $cta-color; + border-radius: 3px; + display: inline-block; + margin: 20px 0; + padding: 12px 24px; + + a { + background-color: $message-background-color; + color: $cta-color; + display: inline-block; + text-decoration: none; + } +} + +#tanuki { + padding: 40px 0 0; + + img { + border: none; + outline: none; + width: 37px; + min-height: 36px; + } +} + +#tagline { + font-size: 22px; + font-weight: 100; + padding: 4px 0 40px; +} + +#social { + padding: 0 10px 20px; + width: 600px; + word-spacing: 20px; + + a { + color: $footer-link-color; + text-decoration: none; + } +} diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss new file mode 100644 index 00000000000..001994db97b --- /dev/null +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -0,0 +1,43 @@ +@import "framework/variables"; + +table.code { + width: 100%; + font-family: monospace; + border: none; + border-collapse: separate; + margin: 0; + padding: 0; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + + td { + line-height: $code_line_height; + font-family: monospace; + font-size: $code_font_size; + } + + td.diff-line-num { + margin: 0; + padding: 0; + border: none; + background: $background-color; + color: rgba(0, 0, 0, 0.3); + padding: 0 5px; + border-right: 1px solid $border-color; + text-align: right; + min-width: 35px; + max-width: 50px; + width: 35px; + } + + td.line_content { + display: block; + margin: 0; + padding: 0 0.5em; + border: none; + white-space: pre; + } +} + +@import "highlight/white"; diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index c2cd227571f..fc3f214aba5 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -26,8 +26,28 @@ .commit-info-row { margin-bottom: 10px; + + &.commit-info-row-header { + line-height: 34px; + + @media (min-width: $screen-sm-min) { + margin-bottom: 0; + } + + .commit-options-dropdown-caret { + @media (max-width: $screen-sm) { + margin-left: 0; + } + } + } + .avatar { @extend .avatar-inline; + margin-left: 0; + + @media (min-width: $screen-sm-min) { + margin-left: 4px; + } } .commit-committer-link, .commit-author-link { @@ -35,10 +55,6 @@ font-weight: bold; } - .time_ago { - margin-left: 8px; - } - .fa-clipboard { color: $dropdown-title-btn-color; } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5e61e61d85c..1b389d83525 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -29,8 +29,6 @@ margin-top: 6px; p { - overflow-x: auto; - &:last-child { margin-bottom: 0; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 8981f070a20..22679c764dc 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -23,7 +23,7 @@ .file-title { @extend .monospace; - line-height: 42px; + line-height: 35px; padding-top: 7px; padding-bottom: 7px; @@ -43,7 +43,7 @@ .editor-file-name { @extend .monospace; - + float: left; margin-right: 10px; } @@ -59,7 +59,22 @@ } .encoding-selector, - .license-selector { + .license-selector, + .gitignore-selector { display: inline-block; + vertical-align: top; + font-family: $regular_font; + } + + .gitignore-selector { + + .dropdown { + line-height: 21px; + } + + .dropdown-menu-toggle { + vertical-align: top; + width: 220px; + } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1cf3023ecc9..787c387379e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -125,7 +125,7 @@ .right-sidebar { position: fixed; - top: 58px; + top: $header-height; bottom: 0; right: 0; z-index: 10; @@ -150,6 +150,10 @@ font-weight: 600; } + .light { + font-weight: normal; + } + .sidebar-collapsed-icon { display: none; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index fc9db97132d..4e35ca329e4 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -40,11 +40,6 @@ } } -.issue-search-form { - margin: 0; - height: 24px; -} - form.edit-issue { margin: 0; } @@ -96,8 +91,3 @@ form.edit-issue { .issue-form .select2-container { width: 250px !important; } - -.issue-closed-by-widget { - color: $gl-text-color; - margin-left: 52px; -} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c4005ba1e69..8046e203a99 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -41,7 +41,7 @@ margin: 0; margin-left: 20px; padding: 5px; - padding-top: 12px; + padding-top: 8px; line-height: 20px; &.right { @@ -110,6 +110,29 @@ p:last-child { margin-bottom: 0; } + + @media (max-width: $screen-sm-max) { + h4 { + font-size: 15px; + } + + p { + font-size: 13px; + } + + .btn, + .btn-group, + .accept-action { + width: 100%; + margin-bottom: 4px; + } + + .accept-control { + width: 100%; + text-align: center; + margin: 0; + } + } } .mr-widget-footer { @@ -280,11 +303,5 @@ background-color: $white-light; color: $gl-placeholder-color; } - - th, - td { - padding: 16px; - } } } - diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 624c8249f7e..a3e1ac13a43 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -226,8 +226,7 @@ ul.notes { } } -.note-action-button, -.discussion-action-button { +.note-action-button { display: inline-block; margin-left: 10px; line-height: 24px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss new file mode 100644 index 00000000000..6128868b670 --- /dev/null +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -0,0 +1,24 @@ +.pipelines { + .stage { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .duration, .finished_at { + margin: 4px 0; + } + + .commit-title { + margin: 0; + } + + .controls { + white-space: nowrap; + } + + .btn { + margin: 4px; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index abc5a0e9877..167ab40d881 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -66,12 +66,6 @@ } } -.calendar-hint { - margin-top: -12px; - float: right; - font-size: 12px; -} - .profile-link-holder { display: inline; @@ -134,14 +128,6 @@ } } -.change-username-title { - color: $gl-warning; -} - -.remove-account-title { - color: $gl-danger; -} - .provider-btn-group { display: inline-block; margin-right: 10px; @@ -218,7 +204,7 @@ .btn { display: inline-block; - width: 48%; + width: 46%; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c20f04653fc..edef336481d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -7,10 +7,10 @@ } .no-ssh-key-message, .project-limit-message { background-color: #f28d35; - margin-bottom: 16px; + margin-bottom: 0; } .new_project, -.edit_project { +.edit-project { fieldset.features { .control-label { font-weight: normal; @@ -26,8 +26,13 @@ } .project-home-panel { - padding-bottom: 40px; - border-bottom: 1px solid $border-color; + background: $white-light; + text-align: left; + padding: 24px 0; + + .container-fluid { + position: relative; + } .cover-controls { .project-settings-dropdown { @@ -43,21 +48,55 @@ } } - .project-identicon-holder { - margin-bottom: 16px; + .cover-title { + margin-bottom: 0; + } - .avatar, .identicon { - margin: 0 auto; - float: none; + .project-image-container { + @include make-sm-column(1); + max-width: 86px; + min-width: 86px; + padding-right: 0; + margin: 11px 0; + + @media (max-width: $screen-md-max) { + padding-left: 0; + margin: 0 0 10px; + max-width: none; + min-width: none; + + .avatar.s70 { + margin: auto; + } } + } - .identicon { - @include border-radius(50%); + .project-info { + @include make-sm-column(10); + + h1 { + font-size: 24px; + font-weight: normal; + margin: 0; + } + + .project-home-desc { + p { + margin: 0; + } } } + .identicon { + float: left; + @include border-radius(50%); + } + + .avatar { + float: none; + } + .notifications-btn { - margin-top: -28px; .fa-bell { margin-right: 6px; @@ -69,28 +108,45 @@ } .project-repo-buttons { - margin-top: 20px; - margin-bottom: 0; + font-size: 0; - .count-buttons { - display: block; - margin-bottom: 20px; - } + .btn { + @include btn-gray; + padding: 3px 10px; + text-transform: none; + background-color: $background-color; - .clone-row { - .split-repo-buttons, - .project-clone-holder { - display: inline-block; + .fa { + color: $layout-link-gray; } - .split-repo-buttons { - margin: 0 12px; + .fa-caret-down { + margin-left: 3px; } } - .btn { - @include btn-gray; - text-transform: none; + .btn-group:not(:first-child):not(:last-child) > .btn { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + + form { + margin-left: 10px; + } + + .count-buttons { + display: inline-block; + vertical-align: top; + margin-top: 16px; + } + + .project-clone-holder { + display: inline-block; + margin-top: 16px; + + input { + height: 29px; + } } .count-with-arrow { @@ -140,14 +196,18 @@ line-height: 13px; padding: $gl-vert-padding $gl-padding; letter-spacing: .4px; - padding: 10px 14px; + padding: 7px 14px; text-align: center; vertical-align: middle; touch-action: manipulation; cursor: pointer; background-image: none; white-space: nowrap; - margin: 0 11px 0 4px; + margin: 0 10px 0 4px; + + a { + color: inherit; + } &:hover { background: #fff; @@ -155,13 +215,37 @@ } } } + + .project-right-buttons { + position: absolute; + right: 16px; + bottom: 0; + + .btn { + padding: 3px 10px; + background-color: $background-color; + } + + @media (max-width: 1304px) { + top: 0; + } + } + + @media (max-width: $screen-md-max) { + text-align: center; + + .project-info, + .project-image-container { + width: 100%; + } + } } .split-one { display: inline-table; margin-right: 12px; - a { + > a { margin: -1px; } } @@ -285,11 +369,11 @@ a.deploy-project-label { } .project-stats { - text-align: center; margin-top: $gl-padding; margin-bottom: 0; - padding-top: 10px; - padding-bottom: 4px; + padding: 16px 0; + background-color: $white-light; + font-size: 0; ul.nav { display: inline-block; @@ -300,12 +384,11 @@ a.deploy-project-label { } .nav > li > a { - @include btn-default; - @include btn-gray; - background-color: transparent; - border: 1px solid #f7f8fa; - margin-left: 12px; + margin-right: 12px; + padding: 0 10px; + font-size: 15px; + color: $notes-light-color; } li { @@ -325,6 +408,10 @@ a.deploy-project-label { background-color: #f0f2f5; } } + + &.row-content-block.second-block { + margin-top: 0; + } } pre.light-well { @@ -442,9 +529,14 @@ pre.light-well { border-top: 0; .edit-project-readme { - z-index: 100; + z-index: 2; position: relative; } + + .wiki h1 { + border-bottom: none; + padding: 0; + } } .git-clone-holder { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 2bff70c8c64..037ad520545 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -28,6 +28,7 @@ } .search-input { + padding-right: 20px; border: none; font-size: 14px; outline: none; @@ -47,6 +48,7 @@ display: inline-block; background-color: $location-badge-bg; vertical-align: top; + cursor: default; } .search-input-container { @@ -55,7 +57,7 @@ position: relative; } - .search-location-badge, .search-input-wrap { + .search-input-wrap { // Fallback if flexbox is not supported display: inline-block; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 3fb70085713..2e8f356298d 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -12,3 +12,11 @@ border: 1px solid $warning-message-border; border-radius: $border-radius-base; } + +.warning-title { + color: $gl-warning; +} + +.danger-title { + color: $gl-danger; +} diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 639d639d5b0..2aa939b7dc3 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -16,19 +16,6 @@ } } -.snippet-box { - @include border-radius(2px); - - display: block; - float: left; - padding: 0 $gl-padding; - font-weight: normal; - margin-right: 10px; - font-size: $gl-font-size; - border: 1px solid; - line-height: 32px; -} - .markdown-snippet-copy { position: fixed; top: -10px; @@ -36,3 +23,34 @@ max-height: 0; max-width: 0; } + +.file-holder.snippet-file-content { + padding-bottom: $gl-padding; + border-bottom: 1px solid $border-color; + + .file-title { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } + + .file-actions { + top: 12px; + } + + .file-content { + border-left: 1px solid $border-color; + border-right: 1px solid $border-color; + border-bottom: 1px solid $border-color; + } +} + +.snippet-title { + font-size: 24px; + font-weight: normal; +} + +.snippet-actions { + @media (min-width: $screen-sm-min) { + float: right; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e51c3491dae..afc00a68572 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -29,6 +29,17 @@ .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); + overflow: visible; + } + + .status-box { + margin: 0; + float: none; + display: inline-block; + font-weight: normal; + padding: 0 5px; + line-height: inherit; + font-size: 14px; } .todo-body { @@ -76,12 +87,11 @@ @media (max-width: $screen-xs-max) { .todo-item { - padding-left: $gl-padding; - .todo-title { white-space: normal; overflow: visible; max-width: 100%; + margin-bottom: 10px; } .avatar { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index a84fc2e0318..f16fc7f388f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -15,16 +15,23 @@ margin-bottom: 0; tr { - > td, > th { + border-bottom: 1px solid $table-border-gray; + border-top: 1px solid $table-border-gray; + + td, th { line-height: 23px; } &:hover { + cursor: pointer; + td { - background: $row-hover; + background-color: $row-hover; + border-top: 1px solid $row-hover-border; + border-bottom: 1px solid $row-hover-border; } - cursor: pointer; } + &.selected { td { background: $gray-dark; diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index e9b0972bdd8..5055c318a5f 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController abuse_report.remove_user(deleted_by: current_user) if params[:remove_user] abuse_report.destroy - render nothing: true + head :ok end end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ec22548ddeb..0a34a12e2a7 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def reset_health_check_token + @application_setting.reset_health_check_access_token! + flash[:notice] = 'New health check access token has been generated!' + redirect_to :back + end + def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async @@ -53,6 +59,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) + + params[:application_setting][:disabled_oauth_sign_in_sources] = + AuthHelper.button_based_providers.map(&:to_s) - + Array(enabled_oauth_sign_in_sources) + params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, @@ -94,8 +106,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, + :send_user_confirmation_email, + :container_registry_token_expire_delay, restricted_visibility_levels: [], - import_sources: [] + import_sources: [], + disabled_oauth_sign_in_sources: [] ) end end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index fc342924987..82055006ac0 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_default(default: { action: 'index' }) } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb new file mode 100644 index 00000000000..241c7be0ea1 --- /dev/null +++ b/app/controllers/admin/health_check_controller.rb @@ -0,0 +1,5 @@ +class Admin::HealthCheckController < Admin::ApplicationController + def show + @errors = HealthCheck::Utils.process_checks('standard') + end +end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index cb33fdd9763..054bb52b696 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController respond_to do |format| format.html - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index a701d49b844..7345c91f67d 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -9,23 +9,18 @@ class Admin::RunnersController < Admin::ApplicationController end def show - @builds = @runner.builds.order('id DESC').first(30) - @projects = - if params[:search].present? - ::Project.search(params[:search]) - else - Project.all - end - @projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any? - @projects = @projects.page(params[:page]).per(30) + assign_builds_and_projects end def update - @runner.update_attributes(runner_params) - - respond_to do |format| - format.js - format.html { redirect_to admin_runner_path(@runner) } + if @runner.update_attributes(runner_params) + respond_to do |format| + format.js + format.html { redirect_to admin_runner_path(@runner) } + end + else + assign_builds_and_projects + render 'show' end end @@ -58,6 +53,18 @@ class Admin::RunnersController < Admin::ApplicationController end def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) + end + + def assign_builds_and_projects + @builds = runner.builds.order('id DESC').first(30) + @projects = + if params[:search].present? + ::Project.search(params[:search]) + else + Project.all + end + @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any? + @projects = @projects.page(params[:page]).per(30) end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 377e9741e5f..3a2f0185315 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." else spam_log.destroy - render nothing: true + head :ok end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f2f654c7bcd..f35f4a8c811 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -119,6 +119,7 @@ class Admin::UsersController < Admin::ApplicationController user_params_with_pass.merge!( password: params[:user][:password], password_confirmation: params[:user][:password_confirmation], + password_expires_at: Time.now ) end @@ -153,7 +154,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 17b3f49aed1..c28d1ca9e3b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -176,7 +176,7 @@ class ApplicationController < ActionController::Base end def check_password_expiration - if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return end end @@ -232,7 +232,7 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) } + devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :email, :password, :login, :remember_me, :otp_attempt]) end def hexdigest(string) @@ -263,7 +263,7 @@ class ApplicationController < ActionController::Base # internal repos where you are not a member. Enable this filter # or improve current implementation to filter only issues you # created or assigned or mentioned - #@filter_params[:authorized_only] = true + # @filter_params[:authorized_only] = true end @filter_params diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index eb0abc80ab4..3865b2d61fd 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -31,6 +31,24 @@ class AutocompleteController < ApplicationController render json: @user, only: [:name, :username, :id], methods: [:avatar_url] end + def projects + project = Project.find_by_id(params[:project_id]) + + projects = current_user.authorized_projects + projects = projects.select do |project| + current_user.can?(:admin_issue, project) + end + + no_project = { + id: 0, + name_with_namespace: 'No project', + } + projects.unshift(no_project) + projects.delete(project) + + render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) + end + private def find_users diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 787416c17ab..dacb5679dd3 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -122,7 +122,7 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch ||= @ref end end end diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 8a43c0b93c4..9e3b9be2ff4 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -6,7 +6,7 @@ module ToggleSubscriptionAction subscribable_resource.toggle_subscription(current_user) - render nothing: true + head :ok end private diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index 23a4ef21ea2..2a88350a4ca 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,6 +1,6 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = Label.where(project_id: projects).select(:title, :color).uniq(:title) + labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) respond_to do |format| format.json { render json: labels } diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 71acc244a91..c08eb811532 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -28,7 +28,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def starred - @projects = current_user.starred_projects.sorted_by_activity + @projects = current_user.viewable_starred_projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 5abf97342c3..f9a1929c117 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -12,7 +12,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: todo_notice } - format.js { render nothing: true } + format.js { head :ok } format.json do render json: { count: @todos.size, done_count: current_user.todos.done.count } end @@ -24,7 +24,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } - format.js { render nothing: true } + format.js { head :ok } format.json do find_todos render json: { count: @todos.size, done_count: current_user.todos.done.count } diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 1dce4a21729..4dda4e51f6a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -25,7 +25,7 @@ class DashboardController < Dashboard::ApplicationController def load_events projects = if params[:filter] == "starred" - current_user.starred_projects + current_user.viewable_starred_projects else current_user.authorized_projects end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index d5ef33888c6..48dbf656e84 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -40,7 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb new file mode 100644 index 00000000000..037da7d2bce --- /dev/null +++ b/app/controllers/health_check_controller.rb @@ -0,0 +1,22 @@ +class HealthCheckController < HealthCheck::HealthCheckController + before_action :validate_health_check_access! + + private + + def validate_health_check_access! + render_404 unless token_valid? + end + + def token_valid? + token = params[:token].presence || request.headers['TOKEN'] + token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare( + token, + current_application_settings.health_check_access_token + ) + end + + def render_404 + render file: Rails.root.join('public', '404'), layout: false, status: '404' + end +end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb new file mode 100644 index 00000000000..cee3b6c43e7 --- /dev/null +++ b/app/controllers/jwt_controller.rb @@ -0,0 +1,87 @@ +class JwtController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + before_action :authenticate_project_or_user + + SERVICES = { + Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService, + } + + def auth + service = SERVICES[params[:service]] + return head :not_found unless service + + result = service.new(@project, @user, auth_params).execute + + render json: result, status: result[:http_status] + end + + private + + def authenticate_project_or_user + authenticate_with_http_basic do |login, password| + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + return if @project + + @user = authenticate_user(login, password) + return if @user + + render_403 + end + end + + def auth_params + params.permit(:service, :scope, :account, :client_id) + end + + def authenticate_project(login, password) + if login == 'gitlab-ci-token' + Project.find_by(builds_enabled: true, runners_token: password) + end + end + + def authenticate_user(login, password) + # TODO: this is a copy and paste from grack_auth, + # it should be refactored in the future + + user = Gitlab::Auth.new.find(login, password) + + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. + config = Gitlab.config.rack_attack.git_basic_auth + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(request.ip, config) + else + banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + return + end + end + end + + user + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index df98f56a1cd..f35d631df0c 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -97,7 +97,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController handle_signup_error end - def handle_service_ticket provider, ticket + def handle_service_ticket(provider, ticket) Gitlab::OAuth::Session.create provider, ticket session[:service_tickets] ||= {} session[:service_tickets][provider] = ticket diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 0ede9b8e21b..1c24c4db993 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_emails_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index a12549d6bcb..830e0b9591b 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -32,7 +32,7 @@ class Profiles::KeysController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_keys_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index be872a93fee..776ba92c9ab 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -26,7 +26,7 @@ class Projects::ApplicationController < ApplicationController project_path = "#{namespace}/#{id}" @project = Project.find_with_namespace(project_path) - if @project && can?(current_user, :read_project, @project) + if can?(current_user, :read_project, @project) && !@project.pending_delete? if @project.path_with_namespace != project_path redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d09e7375b67..dd9508da049 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303 end - format.js { render status: status[:return_code] } + format.js { render nothing: true, status: status[:return_code] } end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index b8b9e78427d..bb1f6c5e980 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -38,6 +38,14 @@ class Projects::BuildsController < Projects::ApplicationController end end + def trace + respond_to do |format| + format.json do + render json: @build.trace_with_state(params[:state]).merge!(id: @build.id, status: @build.status) + end + end + end + def retry unless @build.retryable? return render_404 diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index a202cb38692..10b5932affa 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -17,12 +17,12 @@ class Projects::CommitController < Projects::ApplicationController def show apply_diff_view_cookie! - @line_notes = commit.notes.inline + @grouped_diff_notes = commit.notes.grouped_diff_notes + @note = @project.build_commit_note(commit) - @notes = commit.notes.not_inline.fresh + @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_allowed = @reply_allowed = true - @comments_target = { + @comments_target = { noteable_type: 'Commit', commit_id: @commit.id } @@ -67,10 +67,10 @@ class Projects::CommitController < Projects::ApplicationController create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end - + def cherry_pick assign_change_commit_vars(@commit.cherry_pick_branch_name) - + return render_404 if @target_branch.blank? create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 671d5c23024..af0b69a2442 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -22,7 +22,8 @@ class Projects::CompareController < Projects::ApplicationController @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diffs = compare.diffs(diff_options) @diff_refs = [@base_commit, @commit] - @line_notes = [] + @diff_notes_disabled = true + @grouped_diff_notes = {} end end diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb new file mode 100644 index 00000000000..d1f46497207 --- /dev/null +++ b/app/controllers/projects/container_registry_controller.rb @@ -0,0 +1,34 @@ +class Projects::ContainerRegistryController < Projects::ApplicationController + before_action :verify_registry_enabled + before_action :authorize_read_container_image! + before_action :authorize_update_container_image!, only: [:destroy] + layout 'project' + + def index + @tags = container_registry_repository.tags + end + + def destroy + url = namespace_project_container_registry_index_path(project.namespace, project) + + if tag.delete + redirect_to url + else + redirect_to url, alert: 'Failed to remove tag' + end + end + + private + + def verify_registry_enabled + render_404 unless Gitlab.config.registry.enabled + end + + def container_registry_repository + @container_registry_repository ||= project.container_registry_repository + end + + def tag + @tag ||= container_registry_repository.tag(params[:id]) + end +end diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb index 54a0c447aee..cf53ad0a670 100644 --- a/app/controllers/projects/find_file_controller.rb +++ b/app/controllers/projects/find_file_controller.rb @@ -1,26 +1,26 @@ -# Controller for viewing a repository's file structure -class Projects::FindFileController < Projects::ApplicationController - include ExtractsPath - include ActionView::Helpers::SanitizeHelper - include TreeHelper - - before_action :require_non_empty_project - before_action :assign_ref_vars - before_action :authorize_download_code! - - def show - return render_404 unless @repository.commit(@ref) - - respond_to do |format| - format.html - end - end - - def list - file_paths = @repo.ls_files(@ref) - - respond_to do |format| - format.json { render json: file_paths } - end - end -end +# Controller for viewing a repository's file structure +class Projects::FindFileController < Projects::ApplicationController + include ExtractsPath + include ActionView::Helpers::SanitizeHelper + include TreeHelper + + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! + + def show + return render_404 unless @repository.commit(@ref) + + respond_to do |format| + format.html + end + end + + def list + file_paths = @repo.ls_files(@ref) + + respond_to do |format| + format.json { render json: file_paths } + end + end +end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index dfa9bd259e8..a60027ff477 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -27,8 +27,10 @@ class Projects::HooksController < Projects::ApplicationController if !@project.empty_repo? status, message = TestHookService.new.execute(hook, current_user) - if status - flash[:notice] = 'Hook successfully executed.' + if status && status >= 200 && status < 400 + flash[:notice] = "Hook executed successfully: HTTP #{status}" + elsif status + flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}" else flash[:alert] = "Hook execution failed: #{message}" end @@ -61,7 +63,8 @@ class Projects::HooksController < Projects::ApplicationController :push_events, :tag_push_events, :token, - :url + :url, + :wiki_page_events ) end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 7756f0f0ed3..a1b84afcd91 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -20,6 +20,7 @@ class Projects::ImportsController < Projects::ApplicationController @project.import_retry else @project.import_start + @project.add_import_job end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9c147b3689e..d54284d7b20 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -73,12 +73,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController # but we need it for the "View file @ ..." link by deleted files @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit - @comments_allowed = @reply_allowed = true @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id } - @line_notes = @merge_request.notes.where("line_code is not null") + + @grouped_diff_notes = @merge_request.notes.grouped_diff_notes respond_to do |format| format.html @@ -117,6 +117,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare + @diff_notes_disabled = true @ci_commit = @merge_request.ci_commit @statuses = @ci_commit.statuses if @ci_commit @@ -204,7 +205,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def branch_from - #This is always source + # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project @commit = @repository.commit(params[:ref]) if params[:ref].present? render layout: false @@ -228,6 +229,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController if ci_commit status = ci_commit.status coverage = ci_commit.try(:coverage) + + status ||= "preparing" else ci_service = @merge_request.source_project.ci_service status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service @@ -237,8 +240,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - status = "preparing" if status.nil? - response = { title: merge_request.title, sha: merge_request.last_commit_short_sha, @@ -300,7 +301,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh - @discussions = Note.discussions_from_notes(@notes) + @discussions = @notes.discussions @noteable = @merge_request # Get commits from repository @@ -333,7 +334,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController params.require(:merge_request).permit( :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, - :state_event, :description, :task_num, label_ids: [] + :state_event, :description, :task_num, :force_remove_source_branch, + label_ids: [] ) end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index f7b6d137bde..da2892bfb3f 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -75,7 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_milestones_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 707a0d0e5c6..40b24d550e0 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -43,7 +43,7 @@ class Projects::NotesController < Projects::ApplicationController end respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end @@ -52,7 +52,7 @@ class Projects::NotesController < Projects::ApplicationController note.update_attribute(:attachment, nil) respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end @@ -96,7 +96,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) - return unless note.for_diff_line? + return unless note.diff_note? if params[:view] == 'parallel' template = "projects/notes/_diff_notes_with_reply_parallel" @@ -120,7 +120,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_with_diff_html(note) - return unless note.for_diff_line? + return unless note.diff_note? render_to_string( "projects/notes/_discussion", @@ -158,7 +158,7 @@ class Projects::NotesController < Projects::ApplicationController def note_params params.require(:note).permit( :note, :noteable, :noteable_id, :noteable_type, :project_id, - :attachment, :line_code, :commit_id + :attachment, :line_code, :commit_id, :type ) end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb new file mode 100644 index 00000000000..b36081205d8 --- /dev/null +++ b/app/controllers/projects/pipelines_controller.rb @@ -0,0 +1,59 @@ +class Projects::PipelinesController < Projects::ApplicationController + before_action :pipeline, except: [:index, :new, :create] + before_action :commit, only: [:show] + before_action :authorize_read_pipeline! + before_action :authorize_create_pipeline!, only: [:new, :create] + before_action :authorize_update_pipeline!, only: [:retry, :cancel] + + def index + @scope = params[:scope] + all_pipelines = project.ci_commits + @pipelines_count = all_pipelines.count + @running_or_pending_count = all_pipelines.running_or_pending.count + @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope) + @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30) + end + + def new + @pipeline = project.ci_commits.new(ref: @project.default_branch) + end + + def create + @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute + unless @pipeline.persisted? + render 'new' + return + end + + redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline) + end + + def show + end + + def retry + pipeline.retry_failed + + redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) + end + + def cancel + pipeline.cancel_running + + redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) + end + + private + + def create_params + params.require(:pipeline).permit(:ref) + end + + def pipeline + @pipeline ||= project.ci_commits.find_by!(id: params[:id]) + end + + def commit + @commit ||= @pipeline.commit_data + end +end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 33b2625c0ac..cdea5f0b776 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController format.html do redirect_to namespace_project_project_members_path(@project.namespace, @project) end - format.js { render nothing: true } + format.js { head :ok } end end @@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html { redirect_to dashboard_projects_path, notice: "You left the project." } - format.js { render nothing: true } + format.js { head :ok } end else if current_user == @project.owner diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index e49259c34b6..efa7bf14d0f 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 0dd2d6a99be..0b4fa572501 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -20,7 +20,7 @@ class Projects::RunnersController < Projects::ApplicationController if @runner.update_attributes(runner_params) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else - redirect_to runner_path(@runner), alert: 'Runner was not updated.' + render 'edit' end end @@ -64,6 +64,6 @@ class Projects::RunnersController < Projects::ApplicationController end def runner_params - params.require(:runner).permit(:description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 00234654578..6f068729390 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -3,20 +3,44 @@ class Projects::VariablesController < Projects::ApplicationController layout 'project_settings' + def index + @variable = Ci::Variable.new + end + def show + @variable = @project.variables.find(params[:id]) end def update - if project.update_attributes(project_params) + @variable = @project.variables.find(params[:id]) + + if @variable.update_attributes(project_params) + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully updated.' + else + render action: "show" + end + end + + def create + @variable = Ci::Variable.new(project_params) + + if @variable.valid? && @project.variables << @variable redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' else - render action: 'show' + render action: "index" end end + def destroy + @key = @project.variables.find(params[:id]) + @key.destroy + + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.' + end + private def project_params - params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + params.require(:variable).permit([:id, :key, :value, :_destroy]) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 0d6c32fabd2..4b404eb03fa 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -91,8 +91,8 @@ class Projects::WikisController < Projects::ApplicationController def markdown_preview text = params[:text] - ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) - ext.analyze(text) + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text, author: current_user) render json: { body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142a..f94e2a84fa2 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,13 +101,7 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do - if current_user - @membership = @project.team.find_member(current_user.id) - - if @membership - @notification_setting = current_user.notification_settings_for(@project) - end - end + @notification_setting = current_user.notification_settings_for(@project) if current_user if @project.repository_exists? if @project.empty_repo? @@ -147,6 +141,7 @@ class ProjectsController < Projects::ApplicationController @suggestions = { emojis: AwardEmoji.urls, issues: autocomplete.issues, + milestones: autocomplete.milestones, mergerequests: autocomplete.merge_requests, members: participants } @@ -202,8 +197,8 @@ class ProjectsController < Projects::ApplicationController def markdown_preview text = params[:text] - ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) - ext.analyze(text) + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text, author: current_user) render json: { body: view_context.markdown(text), @@ -235,7 +230,8 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, + :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :public_builds, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 352bff19383..75b78a49eab 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -37,8 +37,8 @@ class RegistrationsController < Devise::RegistrationsController super end - def after_sign_up_path_for(_resource) - users_almost_there_path + def after_sign_up_path_for(user) + user.confirmed? ? dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(_resource) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index c29f4609e93..d68c2a708e3 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,5 +1,6 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor + include Devise::Controllers::Rememberable include Recaptcha::ClientHelper skip_before_action :check_2fa_requirement, only: [:destroy] @@ -96,6 +97,7 @@ class SessionsController < Devise::SessionsController # Remove any lingering user data from login session.delete(:otp_user_id) + remember_me(user) if user_params[:remember_me] == '1' sign_in(user) and return else flash.now[:alert] = 'Invalid two-factor code.' diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2daceed039b..2a17c1f34db 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,7 +10,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ae180c8a12..a99632454d9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -58,11 +58,22 @@ class UsersController < ApplicationController end end + def snippets + load_snippets + + respond_to do |format| + format.html { render 'show' } + format.json do + render json: { + html: view_to_html_string("snippets/_snippets", collection: @snippets) + } + end + end + end + def calendar calendar = contributions_calendar @timestamps = calendar.timestamps - @starting_year = calendar.starting_year - @starting_month = calendar.starting_month render 'calendar', layout: false end @@ -116,6 +127,15 @@ class UsersController < ApplicationController @groups = JoinedGroupsFinder.new(user).execute(current_user) end + def load_snippets + @snippets = SnippetsFinder.new.execute( + current_user, + filter: :by_user, + user: user, + scope: params[:scope] + ).page(params[:page]) + end + def projects_for_current_user ProjectsFinder.new.execute(current_user) end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 3b9a421b118..aa8f4c1d0e4 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder projects = [] if current_user - if @group.users.include?(current_user) + if @group.users.include?(current_user) || current_user.admin? projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f00f3f709e9..7d8c56f4c22 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -250,12 +250,12 @@ class IssuableFinder def by_milestone(items) if milestones? if filter_by_no_milestone? - items = items.where(milestone_id: [-1, nil]) + items = items.left_joins_milestones.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? - upcoming = Milestone.where(project_id: projects).upcoming - items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) }) + upcoming_ids = Milestone.upcoming_ids_by_projects(projects) + items = items.left_joins_milestones.where(milestone_id: upcoming_ids) else - items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) + items = items.with_milestone(params[:milestone_title]) if projects items = items.where(milestones: { project_id: projects }) @@ -271,7 +271,7 @@ class IssuableFinder if filter_by_no_label? items = items.without_label else - items = items.with_label(label_names) + items = items.with_label(label_names, params[:sort]) if projects items = items.where(labels: { project_id: projects }) end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index fa4c635f55c..c41be333537 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -10,7 +10,7 @@ class NotesFinder notes = case target_type when "commit" - project.notes.for_commit_id(target_id).not_inline + project.notes.for_commit_id(target_id).non_diff_notes when "issue" project.issues.find(target_id).notes.nonawards.inc_author when "merge_request" diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb new file mode 100644 index 00000000000..c19a795d467 --- /dev/null +++ b/app/finders/pipelines_finder.rb @@ -0,0 +1,38 @@ +class PipelinesFinder + attr_reader :project + + def initialize(project) + @project = project + end + + def execute(pipelines, scope) + case scope + when 'running' + pipelines.running_or_pending + when 'branches' + from_ids(pipelines, ids_for_ref(pipelines, branches)) + when 'tags' + from_ids(pipelines, ids_for_ref(pipelines, tags)) + else + pipelines + end + end + + private + + def ids_for_ref(pipelines, refs) + pipelines.where(ref: refs).group(:ref).select('max(id)') + end + + def from_ids(pipelines, ids) + pipelines.unscoped.where(id: ids) + end + + def branches + project.repository.branches.map(&:name) + end + + def tags + project.repository.tags.map(&:name) + end +end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3ba27c40504..4bd46a76087 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -36,7 +36,7 @@ class TodosFinder private def action_id? - action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i) + action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i) end def action_id diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3e0074da394..439b015b3b8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,8 +110,7 @@ module ApplicationHelper ] # If reference is commit id - we should add it to branch/tag selectbox - if(@ref && !options.flatten.include?(@ref) && - @ref =~ /\A[0-9a-zA-Z]{6,52}\z/) + if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/ options << ['Commit', [@ref]] end @@ -263,6 +262,8 @@ module ApplicationHelper assignee_id: params[:assignee_id], author_id: params[:author_id], sort: params[:sort], + issue_search: params[:issue_search], + label_name: params[:label_name] } options = exist_opts.merge(options) @@ -273,16 +274,11 @@ module ApplicationHelper end end - path = request.path - path << "?#{options.to_param}" - if add_label - if params[:label_name].present? and params[:label_name].respond_to?('any?') - params[:label_name].each do |label| - path << "&label_name[]=#{label}" - end - end - end - path + params = options.compact + + params.delete(:label_name) unless add_label + + "#{request.path}?#{params.to_param}" end def outdated_browser? diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 914b0ef6042..03080d25931 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -60,4 +60,18 @@ module ApplicationSettingsHelper end end end + + def oauth_providers_checkboxes + button_based_providers.map do |source| + disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + css_class = 'btn' + css_class << ' active' unless disabled + checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, source, !disabled, + autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source) + end + end + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index b4f80fd9b3e..b05fa0a14d6 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -38,6 +38,16 @@ module AuthHelper auth_providers.reject { |provider| form_based_provider?(provider) } end + def enabled_button_based_providers + disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || [] + + button_based_providers.map(&:to_s) - disabled_providers + end + + def button_based_providers_enabled? + enabled_button_based_providers.any? + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 93241b3afb7..cec2dc753fe 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -184,4 +184,14 @@ module BlobHelper Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } } end + + def gitignore_names + return @gitignore_names if defined?(@gitignore_names) + + @gitignore_names = { + Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } }, + # Note that the key here doesn't cover it really + Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } } + } + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index a9047ede8c5..f742922d926 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -30,7 +30,7 @@ module ButtonHelper content_tag :a, protocol, class: klass, - href: @project.http_url_to_repo, + href: project.http_url_to_repo, data: { html: true, placement: 'right', diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 417050b4132..cfad17dcacf 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -38,19 +38,30 @@ module CiStatusHelper icon(icon_name + ' fw') end - def render_ci_status(ci_commit, tooltip_placement: 'auto left') - # TODO: split this method into - # - render_commit_status - # - render_pipeline_status - link_to ci_icon_for_status(ci_commit.status), - ci_status_path(ci_commit), - class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", - title: "Build #{ci_label_for_status(ci_commit.status)}", - data: { toggle: 'tooltip', placement: tooltip_placement } + def render_commit_status(commit, tooltip_placement: 'auto left') + project = commit.project + path = builds_namespace_project_commit_path(project.namespace, project, commit) + render_status_with_link('commit', commit.status, path, tooltip_placement) + end + + def render_pipeline_status(pipeline, tooltip_placement: 'auto left') + project = pipeline.project + path = namespace_project_pipeline_path(project.namespace, project, pipeline) + render_status_with_link('pipeline', pipeline.status, path, tooltip_placement) end def no_runners_for_project?(project) project.runners.blank? && Ci::Runner.shared.blank? end + + private + + def render_status_with_link(type, status, path, tooltip_placement) + link_to ci_icon_for_status(status), + path, + class: "ci-status-link ci-status-icon-#{status.dasherize}", + title: "#{type.titleize}: #{ci_label_for_status(status)}", + data: { toggle: 'tooltip', placement: tooltip_placement } + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index b59c3982edd..d328f56c80c 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -123,13 +123,14 @@ module CommitsHelper ) end - def revert_commit_link(commit, continue_to_path, btn_class: nil) + def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Revert this #{commit.change_type_title} in a new merge request" + tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip if can_collaborate_with_project? - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil? + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -140,17 +141,20 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip + btn_class = "btn btn-grouped btn-close" unless btn_class.nil? + + link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end - def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil) + def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" if can_collaborate_with_project? - link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil? + link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -161,7 +165,8 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip + btn_class = "btn btn-grouped btn-close" unless btn_class.nil? + link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 9f73edb4553..cbe47176831 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -2,8 +2,8 @@ module DiffHelper def mark_inline_diffs(old_line, new_line) old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs - marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs) - marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs) + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion) + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition) [marked_old_line, marked_new_line] end @@ -39,11 +39,11 @@ module DiffHelper end def unfold_bottom_class(bottom) - (bottom) ? 'js-unfold-bottom' : '' + bottom ? 'js-unfold-bottom' : '' end def unfold_class(unfold) - (unfold) ? 'unfold js-unfold' : '' + unfold ? 'unfold js-unfold' : '' end def diff_line_content(line, line_type = nil) @@ -55,22 +55,18 @@ module DiffHelper end end - def line_comments - @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) - end - - def organize_comments(type_left, type_right, line_code_left, line_code_right) - comments_left = comments_right = nil + def organize_comments(left, right) + notes_left = notes_right = nil - unless type_left.nil? && type_right == 'new' - comments_left = line_comments[line_code_left] + unless left[:type].nil? && right[:type] == 'new' + notes_left = @grouped_diff_notes[left[:line_code]] end - unless type_left.nil? && type_right.nil? - comments_right = line_comments[line_code_right] + unless left[:type].nil? && right[:type].nil? + notes_right = @grouped_diff_notes[right[:line_code]] end - [comments_left, comments_right] + [notes_left, notes_right] end def inline_diff_btn @@ -96,8 +92,8 @@ module DiffHelper ].join(' ').html_safe end - def commit_for_diff(diff) - if diff.deleted_file + def commit_for_diff(diff_file) + if diff_file.deleted_file @base_commit || @commit.parent || @commit else @commit diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 41b5bd7be90..8466d0aa0ba 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -32,12 +32,6 @@ module EmailsHelper nil end - def color_email_diff(diffcontent) - formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') - lexer = Rouge::Lexers::Diff - raw formatter.format(lexer.lex(diffcontent)) - end - def password_reset_token_valid_time valid_hours = Devise.reset_password_within / 60 / 60 if valid_hours >= 24 diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 592bad8ba24..bfedcb1c42b 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,7 +3,7 @@ module EventsHelper author = event.author if author - link_to author.name, user_path(author.username), title: h(author.name) + link_to author.name, user_path(author.username), title: author.name else event.author_name end @@ -39,15 +39,6 @@ module EventsHelper end end - def icon_for_event - { - EventFilter.push => 'upload', - EventFilter.merged => 'check-square-o', - EventFilter.comments => 'comments', - EventFilter.team => 'user', - } - end - def event_preposition(event) if event.push? || event.commented? || event.target "at" @@ -66,11 +57,7 @@ module EventsHelper words << event.ref_name words << "at" elsif event.commented? - if event.note_commit? - words << event.note_short_commit_id - else - words << "##{truncate event.note_target_iid}" - end + words << event.note_target_reference words << "at" elsif event.milestone? words << "##{event.target_iid}" if event.target_iid @@ -93,21 +80,12 @@ module EventsHelper elsif event.merge_request? namespace_project_merge_request_url(event.project.namespace, event.project, event.merge_request) - elsif event.note? && event.note_commit? + elsif event.note? && event.commit_note? namespace_project_commit_url(event.project.namespace, event.project, event.note_target) elsif event.note? if event.note_target - if event.note_commit? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)) - elsif event.note_project_snippet? - namespace_project_snippet_path(event.project.namespace, - event.project, event.note_target) - else - event_note_target_path(event) - end + event_note_target_path(event) end elsif event.push? push_event_feed_url(event) @@ -143,42 +121,30 @@ module EventsHelper end def event_note_target_path(event) - if event.note? && event.note_commit? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_target) + if event.note? && event.commit_note? + namespace_project_commit_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) + elsif event.project_snippet_note? + namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) else polymorphic_path([event.project.namespace.becomes(Namespace), event.project, event.note_target], - anchor: dom_id(event.target)) + anchor: dom_id(event.target)) end end def event_note_title_html(event) if event.note_target - if event.note_commit? - link_to( - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target), title: h(event.target_title)), - class: "commit_short_id" - ) do - "#{event.note_target_type} #{event.note_short_commit_id}" - end - elsif event.note_project_snippet? - link_to(namespace_project_snippet_path(event.project.namespace, - event.project, - event.note_target), title: h(event.project.name)) do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end - else - link_to event_note_target_path(event) do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end + link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do + "#{event.note_target_type} #{event.note_target_reference}" end else - content_tag :strong do - "(deleted)" - end + content_tag(:strong, '(deleted)') end end @@ -193,28 +159,6 @@ module EventsHelper "--broken encoding" end - def event_to_atom(xml, event) - if event.visible_to_user?(current_user) - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(event) - event_summary = event_feed_summary(event) - - xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" - xml.link href: event_link - xml.title truncate(event_title, length: 80) - xml.updated event.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - - xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? } - end - end - end - def event_row_class(event) if event.body? "event-block" diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 3a45205563e..0a1b48af219 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -13,7 +13,7 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body =~ /\A\ opts[:push_code_to_protected_branches], - author_id: opts[:author_id] || '' + author_id: opts[:author_id] || '' } } diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 96a83671009..563ddd2a511 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -95,7 +95,9 @@ module TabHelper end def project_tab_class - return "active" if current_page?(controller: "/projects", action: :edit, id: @project) + if controller.controller_path.start_with?('projects') + return 'active' + end if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name "active" @@ -112,7 +114,7 @@ module TabHelper end def profile_tab_class - if controller.controller_path =~ /\Aprofiles/ + if controller.controller_path.start_with?('profiles') return 'active' end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 2f066682180..b4923fbb138 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -11,12 +11,15 @@ module TodosHelper case todo.action when Todo::ASSIGNED then 'assigned you' when Todo::MENTIONED then 'mentioned you on' + when Todo::BUILD_FAILED then 'The build failed for your' end end def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), + class: 'has-tooltip', + title: todo.target.title end def todo_target_path(todo) @@ -28,8 +31,21 @@ module TodosHelper namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, todo.target, anchor: anchor) else - polymorphic_path([todo.project.namespace.becomes(Namespace), - todo.project, todo.target], anchor: anchor) + path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] + + path.unshift(:builds) if todo.build_failed? + + polymorphic_path(path, anchor: anchor) + end + end + + def todo_target_state_pill(todo) + return unless show_todo_state?(todo) + + content_tag(:span, nil, class: 'target-status') do + content_tag(:span, nil, class: "status-box status-box-#{todo.target.state.dasherize}") do + todo.target.state.capitalize + end end end @@ -91,4 +107,10 @@ module TodosHelper options_from_collection_for_select(types, 'name', 'title', params[:type]) end + + private + + def show_todo_state?(todo) + (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state) + end end diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index b616add283a..415f6e12885 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,4 +1,6 @@ class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to + + layout 'devise_mailer' end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 377c2999d6c..fdf1e9f5afc 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -59,20 +59,20 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, opts = {}) + def repository_push_email(project_id, opts = {}) @message = - Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) + Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts) # used in notify layout @target_url = @message.target_url - @project = Project.find project_id + @project = Project.find(project_id) + @diff_notes_disabled = true add_project_headers headers['X-GitLab-Author'] = @message.author_username mail(from: sender(@message.author_id, @message.send_from_committer_email?), reply_to: @message.reply_to, - to: @message.recipient, subject: @message.subject) end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 826e5f96fa1..1c663bdd521 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -10,6 +10,8 @@ class Notify < BaseMailer include Emails::Builds add_template_helper MergeRequestsHelper + add_template_helper DiffHelper + add_template_helper BlobHelper add_template_helper EmailsHelper def test_email(recipient_email, subject, body) diff --git a/app/models/ability.rb b/app/models/ability.rb index 6103a2947e2..44515550d9e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -23,20 +23,41 @@ class Ability end.concat(global_abilities(user)) end + # Given a list of users and a project this method returns the users that can + # read the given project. + def users_that_can_read_project(users, project) + if project.public? + users + else + users.select do |user| + if user.admin? + true + elsif project.internal? && !user.external? + true + elsif project.owner == user + true + elsif project.team.members.include?(user) + true + else + false + end + end + end + end + # List of possible abilities for anonymous user def anonymous_abilities(user, subject) - case true - when subject.is_a?(PersonalSnippet) + if subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) - when subject.is_a?(ProjectSnippet) + elsif subject.is_a?(ProjectSnippet) anonymous_project_snippet_abilities(subject) - when subject.is_a?(CommitStatus) + elsif subject.is_a?(CommitStatus) anonymous_commit_status_abilities(subject) - when subject.is_a?(Project) || subject.respond_to?(:project) + elsif subject.is_a?(Project) || subject.respond_to?(:project) anonymous_project_abilities(subject) - when subject.is_a?(Group) || subject.respond_to?(:group) + elsif subject.is_a?(Group) || subject.respond_to?(:group) anonymous_group_abilities(subject) - when subject.is_a?(User) + elsif subject.is_a?(User) anonymous_user_abilities else [] @@ -60,7 +81,9 @@ class Ability :read_project_member, :read_merge_request, :read_note, + :read_pipeline, :read_commit_status, + :read_container_image, :download_code ] @@ -203,6 +226,8 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_container_image, + :read_pipeline, ] end @@ -214,9 +239,13 @@ class Ability :update_commit_status, :create_build, :update_build, + :create_pipeline, + :update_pipeline, :create_merge_request, :create_wiki, - :push_code + :push_code, + :create_container_image, + :update_container_image, ] end @@ -242,7 +271,9 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_container_image, + :admin_pipeline ] end @@ -285,6 +316,11 @@ class Ability unless project.builds_enabled rules += named_abilities('build') + rules += named_abilities('pipeline') + end + + unless project.container_registry_enabled + rules += named_abilities('container_image') end rules diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index b61f5123127..b01a244032d 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: abuse_reports -# -# id :integer not null, primary key -# reporter_id :integer -# user_id :integer -# message :text -# created_at :datetime -# updated_at :datetime -# - class AbuseReport < ActiveRecord::Base belongs_to :reporter, class_name: 'User' belongs_to :user diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 4528760fefa..4cf8dd9a8ce 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: appearances -# -# id :integer not null, primary key -# title :string -# description :text -# header_logo :string -# logo :string -# created_at :datetime not null -# updated_at :datetime not null -# - class Appearance < ActiveRecord::Base validates :title, presence: true validates :description, presence: true diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 72ec91d2909..42f908aa344 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,63 +1,13 @@ -# == Schema Information -# -# Table name: application_settings -# -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string -# default_branch_protection :integer default(2) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# admin_notification_email :string -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string -# require_two_factor_authentication :boolean default(FALSE) -# two_factor_grace_period :integer default(48) -# metrics_enabled :boolean default(FALSE) -# metrics_host :string default("localhost") -# metrics_pool_size :integer default(16) -# metrics_timeout :integer default(10) -# metrics_method_call_threshold :integer default(10) -# recaptcha_enabled :boolean default(FALSE) -# recaptcha_site_key :string -# recaptcha_private_key :string -# metrics_port :integer default(8089) -# metrics_sample_interval :integer default(15) -# sentry_enabled :boolean default(FALSE) -# sentry_dsn :string -# akismet_enabled :boolean default(FALSE) -# akismet_api_key :string -# email_author_in_body :boolean default(FALSE) -# default_group_visibility :integer -# repository_checks_enabled :boolean default(FALSE) -# metrics_packet_size :integer default(1) -# shared_runners_text :text -# - class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token + add_authentication_token_field :health_check_access_token CACHE_KEY = 'application_setting.last' serialize :restricted_visibility_levels serialize :import_sources + serialize :disabled_oauth_sign_in_sources, Array serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw @@ -101,6 +51,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :container_registry_token_expire_delay, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -121,7 +75,18 @@ class ApplicationSetting < ActiveRecord::Base end end + validates_each :disabled_oauth_sign_in_sources do |record, attr, value| + unless value.nil? + value.each do |source| + unless Devise.omniauth_providers.include?(source.to_sym) + record.errors.add(attr, "'#{source}' is not an OAuth sign-in source") + end + end + end + end + before_save :ensure_runners_registration_token + before_save :ensure_health_check_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -137,6 +102,10 @@ class ApplicationSetting < ActiveRecord::Base Rails.cache.delete(CACHE_KEY) end + def self.cached + Rails.cache.fetch(CACHE_KEY) + end + def self.create_from_defaults create( default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -159,6 +128,9 @@ class ApplicationSetting < ActiveRecord::Base recaptcha_enabled: false, akismet_enabled: false, repository_checks_enabled: true, + disabled_oauth_sign_in_sources: [], + send_user_confirmation_email: false, + container_registry_token_expire_delay: 5, ) end @@ -185,4 +157,8 @@ class ApplicationSetting < ActiveRecord::Base def runners_registration_token ensure_runners_registration_token! end + + def health_check_access_token + ensure_health_check_access_token! + end end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 44b090260e7..967ffd46db0 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: audit_events -# -# id :integer not null, primary key -# author_id :integer not null -# type :string not null -# entity_id :integer not null -# entity_type :string not null -# details :text -# created_at :datetime -# updated_at :datetime -# - class AuditEvent < ActiveRecord::Base serialize :details, Hash diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 075ac733bfc..61498140f27 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: broadcast_messages -# -# id :integer not null, primary key -# message :text not null -# starts_at :datetime -# ends_at :datetime -# created_at :datetime -# updated_at :datetime -# color :string -# font :string -# - class BroadcastMessage < ActiveRecord::Base include Sortable diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4bc3a225e2c..64723ab6b4b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1,40 +1,3 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string -# user_id :integer -# type :string -# target_url :string -# description :string -# artifacts_file :text -# gl_project_id :integer -# artifacts_metadata :text -# erased_by_id :integer -# erased_at :datetime -# - module Ci class Build < CommitStatus belongs_to :runner, class_name: 'Ci::Runner' @@ -90,6 +53,7 @@ module Ci new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request new_build.save + MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build end end @@ -132,8 +96,12 @@ module Ci end def trace_html - html = Ci::Ansi2html::convert(trace) if trace.present? - html || '' + trace_with_state[:html] || '' + end + + def trace_with_state(state = nil) + trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? + trace_with_state || {} end def timeout @@ -238,7 +206,7 @@ module Ci end def recreate_trace_dir - unless Dir.exists?(dir_to_trace) + unless Dir.exist?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end end @@ -318,14 +286,20 @@ module Ci project.runners_token end - def valid_token? token + def valid_token?(token) project.valid_runners_token? token end def can_be_served?(runner) + return false unless has_tags? || runner.run_untagged? + (tag_list - runner.tag_list).empty? end + def has_tags? + tag_list.any? + end + def any_runners_online? project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } end @@ -339,6 +313,7 @@ module Ci build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + project.running_or_pending_build_count(force: true) end def artifacts? diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 4ac4e0fb8b2..f22b573a94c 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: ci_commits -# -# id :integer not null, primary key -# project_id :integer -# ref :string -# sha :string -# before_sha :string -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime -# gl_project_id :integer -# status :string -# started_at :datetime -# finished_at :datetime -# duration :integer -# - module Ci class Commit < ActiveRecord::Base extend Ci::Model @@ -30,8 +8,6 @@ module Ci has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' - delegate :stages, to: :statuses - validates_presence_of :sha validates_presence_of :status validate :valid_commit_sha @@ -44,7 +20,8 @@ module Ci end def self.stages - CommitStatus.where(commit: all).stages + # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries + CommitStatus.where(commit: pluck(:id)).stages end def project_id @@ -89,6 +66,29 @@ module Ci end end + def cancelable? + builds.running_or_pending.any? + end + + def cancel_running + builds.running_or_pending.each(&:cancel) + end + + def retry_failed + builds.latest.failed.select(&:retryable?).each(&:retry) + end + + def latest? + return false unless ref + commit = project.commit(ref) + return false unless commit + commit.sha == sha + end + + def triggered? + trigger_requests.any? + end + def create_builds(user, trigger_request = nil) return unless config_processor config_processor.stages.any? do |stage| diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index add59a08892..adb65292208 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -1,28 +1,10 @@ -# == Schema Information -# -# Table name: ci_runners -# -# id :integer not null, primary key -# token :string -# created_at :datetime -# updated_at :datetime -# description :string -# contacted_at :datetime -# active :boolean default(TRUE), not null -# is_shared :boolean default(FALSE) -# name :string -# version :string -# revision :string -# platform :string -# architecture :string -# - module Ci class Runner < ActiveRecord::Base extend Ci::Model LAST_CONTACT_TIME = 5.minutes.ago - AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] + AVAILABLE_SCOPES = %w[specific shared active paused online] + FORM_EDITABLE = %i[description tag_list active run_untagged] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' @@ -44,6 +26,8 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end + validate :tag_constraints + acts_as_taggable # Searches for runners matching the given query. @@ -76,7 +60,7 @@ module Ci end def display_name - return short_sha unless !description.blank? + return short_sha if description.blank? description end @@ -114,5 +98,18 @@ module Ci def short_sha token[0...8] if token end + + def has_tags? + tag_list.any? + end + + private + + def tag_constraints + unless has_tags? || run_untagged? + errors.add(:tags_list, + 'can not be empty when runner is not allowed to pick untagged jobs') + end + end end end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 7b16f207a26..4b44ffa886e 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: ci_runner_projects -# -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# gl_project_id :integer -# - module Ci class RunnerProject < ActiveRecord::Base extend Ci::Model diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 4f3f4d79fac..a0b19b51a12 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: ci_triggers -# -# id :integer not null, primary key -# token :string -# project_id :integer -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime -# gl_project_id :integer -# - module Ci class Trigger < ActiveRecord::Base extend Ci::Model diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 9973d2e5ade..872d5fb31de 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: ci_trigger_requests -# -# id :integer not null, primary key -# trigger_id :integer not null -# variables :text -# created_at :datetime -# updated_at :datetime -# commit_id :integer -# - module Ci class TriggerRequest < ActiveRecord::Base extend Ci::Model diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 4229fe085a1..f8d5d4486fd 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: ci_variables -# -# id :integer not null, primary key -# project_id :integer -# key :string -# value :text -# encrypted_value :text -# encrypted_value_salt :string -# encrypted_value_iv :string -# gl_project_id :integer -# - module Ci class Variable < ActiveRecord::Base extend Ci::Model @@ -25,6 +11,9 @@ module Ci format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } - attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + attr_encrypted :value, + mode: :per_attribute_iv_and_salt, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 562c3ed15b2..f96c7cb34d0 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -8,7 +8,10 @@ class Commit include StaticModel attr_mentionable :safe_message, pipeline: :single_line - participant :author, :committer, :notes + + participant :author + participant :committer + participant :notes_with_associations attr_accessor :project @@ -194,6 +197,10 @@ class Commit project.notes.for_commit_id(self.id) end + def notes_with_associations + notes.includes(:author, :project) + end + def method_missing(m, *args, &block) @raw.send(m, *args, &block) end @@ -219,7 +226,7 @@ class Commit def revert_branch_name "revert-#{short_id}" end - + def cherry_pick_branch_name project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end @@ -251,11 +258,13 @@ class Commit end def has_been_reverted?(current_user = nil, noteable = self) - Gitlab::ReferenceExtractor.lazily do - noteable.notes.system.flat_map do |note| - note.all_references(current_user).commits - end - end.any? { |commit_ref| commit_ref.reverts_commit?(self) } + ext = all_references(current_user) + + noteable.notes_with_associations.system.each do |note| + note.all_references(current_user, extractor: ext) + end + + ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } end def change_type_title diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 51673897d98..4066958f67c 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -62,7 +62,7 @@ class CommitRange def initialize(range_string, project) @project = project - range_string.strip! + range_string = range_string.strip unless range_string =~ /\A#{PATTERN}\z/ raise ArgumentError, "invalid CommitRange string format: #{range_string}" diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 1260c448de3..f774b6e0efb 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,40 +1,3 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string -# user_id :integer -# type :string -# target_url :string -# description :string -# artifacts_file :text -# gl_project_id :integer -# artifacts_metadata :text -# erased_by_id :integer -# erased_at :datetime -# - class CommitStatus < ActiveRecord::Base include Statuseable @@ -51,7 +14,8 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } - scope :ordered, -> { order(:ref, :stage_idx, :name) } + scope :retried, -> { where.not(id: latest) } + scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status, initial: :pending do @@ -82,6 +46,10 @@ class CommitStatus < ActiveRecord::Base after_transition [:pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) end + + after_transition any => :failed do |commit_status| + MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status) + end end delegate :sha, :short_sha, to: :commit @@ -91,13 +59,15 @@ class CommitStatus < ActiveRecord::Base end def self.stages - order_by = 'max(stage_idx)' - group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact + # We group by stage name, but order stages by theirs' index + unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') end def self.stages_status - all.stages.inject({}) do |h, stage| - h[stage] = all.where(stage: stage).status + # We execute subquery for each stage to calculate a stage status + statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) + statuses.inject({}) do |h, k| + h[k.first] = k.last h end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2e4efc4e8d8..e86d5236abb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -31,18 +31,21 @@ module Issuable scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } + scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :opened, -> { with_state(:opened, :reopened) } scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } scope :closed, -> { with_state(:closed) } - scope :order_milestone_due_desc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date DESC, milestones.id DESC') } - scope :order_milestone_due_asc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date ASC, milestones.id ASC') } - scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } + scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } + scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') } + scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') } + + scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } - scope :outer_join_milestone, -> { joins("LEFT OUTER JOIN milestones ON milestones.id = #{table_name}.milestone_id") } + delegate :name, :email, @@ -56,11 +59,23 @@ module Issuable prefix: true attr_mentionable :title, pipeline: :single_line - attr_mentionable :description, cache: true - participant :author, :assignee, :notes_with_associations + attr_mentionable :description + + participant :author + participant :assignee + participant :notes_with_associations + strip_attributes :title acts_as_paranoid + + after_save :update_assignee_cache_counts, if: :assignee_id_changed? + + def update_assignee_cache_counts + # make sure we flush the cache for both the old *and* new assignee + User.find(assignee_id_was).update_cache_counts if assignee_id_was + assignee.update_cache_counts if assignee + end end module ClassMethods @@ -123,13 +138,29 @@ module Issuable joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") end - def with_label(title) - if title.is_a?(Array) && title.count > 1 - joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}") + def with_label(title, sort = nil) + if title.is_a?(Array) && title.size > 1 + joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") else joins(:labels).where(labels: { title: title }) end end + + # Includes table keys in group by clause when sorting + # preventing errors in postgres + # + # Returns an array of arel columns + def grouping_columns(sort) + grouping_columns = [arel_table[:id]] + + if ["milestone_due_desc", "milestone_due_asc"].include?(sort) + milestone_table = Milestone.arel_table + grouping_columns << milestone_table[:id] + grouping_columns << milestone_table[:due_date] + end + + grouping_columns + end end def today? @@ -160,6 +191,10 @@ module Issuable notes.awards.where(note: "thumbsup").count end + def user_notes_count + notes.user.count + end + def subscribed_without_subscriptions?(user) participants(user).include?(user) end @@ -178,6 +213,10 @@ module Issuable hook_data end + def labels_array + labels.to_a + end + def label_names labels.order('title ASC').pluck(:title) end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 98f71ae8cb0..f00b5b8497c 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -23,7 +23,7 @@ module Mentionable included do if self < Participable - participant ->(current_user) { mentioned_users(current_user) } + participant -> (user, ext) { all_references(user, extractor: ext) } end end @@ -43,23 +43,22 @@ module Mentionable self end - def all_references(current_user = self.author, text = nil) - ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author) + def all_references(current_user = nil, text = nil, extractor: nil) + extractor ||= Gitlab::ReferenceExtractor. + new(project, current_user || author) if text - ext.analyze(text) + extractor.analyze(text, author: author) else self.class.mentionable_attrs.each do |attr, options| - text = send(attr) + text = __send__(attr) + options = options.merge(cache_key: [self, attr], author: author) - context = options.dup - context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted? - - ext.analyze(text, context) + extractor.analyze(text, options) end end - ext + extractor end def mentioned_users(current_user = nil) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index fc6f83b918b..9056722f45e 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -3,8 +3,6 @@ # Contains functionality related to objects that can have participants, such as # an author, an assignee and people mentioned in its description or comments. # -# Used by Issue, Note, MergeRequest, Snippet and Commit. -# # Usage: # # class Issue < ActiveRecord::Base @@ -12,22 +10,36 @@ # # # ... # -# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) } +# participant :author +# participant :assignee +# participant :notes +# +# participant -> (current_user, ext) do +# ext.analyze('...') +# end # end # # issue = Issue.last # users = issue.participants -# # `users` will contain the issue's author, its assignee, -# # all users returned by its #mentioned_users method, -# # as well as all participants to all of the issue's notes, -# # since Note implements Participable as well. -# module Participable extend ActiveSupport::Concern module ClassMethods - def participant(*attrs) - participant_attrs.concat(attrs) + # Adds a list of participant attributes. Attributes can either be symbols or + # Procs. + # + # When using a Proc instead of a Symbol the Proc will be given two + # arguments: + # + # 1. The current user (as an instance of User) + # 2. An instance of `Gitlab::ReferenceExtractor` + # + # It is expected that a Proc populates the given reference extractor + # instance with data. The return value of the Proc is ignored. + # + # attr - The name of the attribute or a Proc + def participant(attr) + participant_attrs << attr end def participant_attrs @@ -35,42 +47,42 @@ module Participable end end - # Be aware that this method makes a lot of sql queries. - # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author) - participants = - Gitlab::ReferenceExtractor.lazily do - self.class.participant_attrs.flat_map do |attr| - value = - if attr.respond_to?(:call) - instance_exec(current_user, &attr) - else - send(attr) - end + # Returns the users participating in a discussion. + # + # This method processes attributes of objects in breadth-first order. + # + # Returns an Array of User instances. + def participants(current_user = nil) + current_user ||= author + ext = Gitlab::ReferenceExtractor.new(project, current_user) + participants = Set.new + process = [self] - participants_for(value, current_user) - end.compact.uniq - end + until process.empty? + source = process.pop - unless Gitlab::ReferenceExtractor.lazy? - participants.select! do |user| - user.can?(:read_project, project) + case source + when User + participants << source + when Participable + source.class.participant_attrs.each do |attr| + if attr.respond_to?(:call) + source.instance_exec(current_user, ext, &attr) + else + process << source.__send__(attr) + end + end + when Enumerable, ActiveRecord::Relation + # This uses reverse_each so we can use "pop" to get the next value to + # process (in order). Using unshift instead of pop would require + # moving all Array values one index to the left (which can be + # expensive). + source.reverse_each { |obj| process << obj } end end - participants - end - - private + participants.merge(ext.users) - def participants_for(value, current_user = nil) - case value - when User, Banzai::LazyReference - [value] - when Enumerable, ActiveRecord::Relation - value.flat_map { |v| participants_for(v, current_user) } - when Participable - value.participants(current_user) - end + Ability.users_that_can_read_project(participants.to_a, project) end end diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index d5a881b2445..083257f1005 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -36,6 +36,12 @@ module Subscribable update(subscribed: !subscribed?(user)) end + def subscribe(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: true) + end + def unsubscribe(user) subscriptions. find_or_initialize_by(user_id: user.id). diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 43cf625f770..2c525d4cd7a 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: keys -# -# id :integer not null, primary key -# user_id :integer -# created_at :datetime -# updated_at :datetime -# key :text -# title :string -# type :string -# fingerprint :string -# public :boolean default(FALSE), not null -# - class DeployKey < Key has_many :deploy_keys_projects, dependent: :destroy has_many :projects, through: :deploy_keys_projects diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index 18db521741f..ae8486bd9ac 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: deploy_keys_projects -# -# id :integer not null, primary key -# deploy_key_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class DeployKeysProject < ActiveRecord::Base belongs_to :project belongs_to :deploy_key diff --git a/app/models/email.rb b/app/models/email.rb index eae2472f337..32a412ab878 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: emails -# -# id :integer not null, primary key -# user_id :integer not null -# email :string not null -# created_at :datetime -# updated_at :datetime -# - class Email < ActiveRecord::Base include Sortable diff --git a/app/models/event.rb b/app/models/event.rb index 25c7c3e6dc7..716039fb54b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: events -# -# id :integer not null, primary key -# target_type :string -# target_id :integer -# title :string -# data :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# action :integer -# author_id :integer -# - class Event < ActiveRecord::Base include Sortable default_scope { where.not(author_id: nil) } @@ -96,7 +80,7 @@ class Event < ActiveRecord::Base end def target_title - target.title if target && target.respond_to?(:title) + target.try(:title) end def created? @@ -282,28 +266,20 @@ class Event < ActiveRecord::Base branch? && project.default_branch != branch_name end - def note_commit_id - target.commit_id - end - def target_iid target.respond_to?(:iid) ? target.iid : target_id end - def note_short_commit_id - Commit.truncate_sha(note_commit_id) - end - - def note_commit? - target.noteable_type == "Commit" + def commit_note? + target.for_commit? end def issue_note? - note? && target && target.noteable_type == "Issue" + note? && target && target.for_issue? end - def note_project_snippet? - target.noteable_type == "Snippet" + def project_snippet_note? + target.for_snippet? end def note_target @@ -311,19 +287,22 @@ class Event < ActiveRecord::Base end def note_target_id - if note_commit? + if commit_note? target.commit_id else target.noteable_id.to_s end end - def note_target_iid - if note_target.respond_to?(:iid) - note_target.iid + def note_target_reference + return unless note_target + + # Commit#to_reference returns the full SHA, but we want the short one here + if commit_note? + note_target.short_id else - note_target_id - end.to_s + note_target.to_reference + end end def note_target_type diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 9b0c6263a96..9803bae0bee 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: forked_project_links -# -# id :integer not null, primary key -# forked_to_project_id :integer not null -# forked_from_project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class ForkedProjectLink < ActiveRecord::Base belongs_to :forked_to_project, class_name: Project belongs_to :forked_from_project, class_name: Project diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index d4afd8cbe84..fa54e3540d0 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,40 +1,3 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string -# user_id :integer -# type :string -# target_url :string -# description :string -# artifacts_file :text -# gl_project_id :integer -# artifacts_metadata :text -# erased_by_id :integer -# erased_at :datetime -# - class GenericCommitStatus < CommitStatus before_validation :set_default_values diff --git a/app/models/group.rb b/app/models/group.rb index cff76877958..aec92e335e6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,20 +1,3 @@ -# == Schema Information -# -# Table name: namespaces -# -# id :integer not null, primary key -# name :string not null -# path :string not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string -# description :string default(""), not null -# avatar :string -# share_with_group_lock :boolean default(FALSE) -# visibility_level :integer default(20), not null -# - require 'carrierwave/orm/activerecord' class Group < Namespace diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 2b8f34a0568..ba42a8eeb70 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class ProjectHook < WebHook belongs_to :project diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 0e176de5ef8..eef24052a06 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class ServiceHook < WebHook belongs_to :service diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index ad508cbbcb8..777bad1e724 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class SystemHook < WebHook def async_execute(data, hook_name) Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name) diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 8e58c9583ab..8b87b6c3d64 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class WebHook < ActiveRecord::Base include Sortable include HTTParty @@ -60,7 +38,7 @@ class WebHook < ActiveRecord::Base basic_auth: auth) end - [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)] + [response.code, response.to_s] rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") [false, e.to_s] diff --git a/app/models/identity.rb b/app/models/identity.rb index ef4d5f99091..3bacc450e6e 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: identities -# -# id :integer not null, primary key -# extern_uid :string -# provider :string -# user_id :integer -# created_at :datetime -# updated_at :datetime -# - class Identity < ActiveRecord::Base include Sortable include CaseSensitivity diff --git a/app/models/issue.rb b/app/models/issue.rb index abaa509707c..bd0fbc96d18 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: issues -# -# id :integer not null, primary key -# title :string -# assignee_id :integer -# author_id :integer -# project_id :integer -# created_at :datetime -# updated_at :datetime -# position :integer default(0) -# branch_name :string -# description :text -# milestone_id :integer -# state :string -# iid :integer -# updated_by_id :integer -# moved_to_id :integer -# confidential :boolean default(FALSE) -# deleted_at :datetime -# due_date :date -# - require 'carrierwave/orm/activerecord' class Issue < ActiveRecord::Base @@ -119,14 +95,13 @@ class Issue < ActiveRecord::Base end def referenced_merge_requests(current_user = nil) - @referenced_merge_requests ||= {} - @referenced_merge_requests[current_user] ||= begin - Gitlab::ReferenceExtractor.lazily do - [self, *notes].flat_map do |note| - note.all_references(current_user).merge_requests - end - end.sort_by(&:iid).uniq + ext = all_references(current_user) + + notes_with_associations.each do |object| + object.all_references(current_user, extractor: ext) end + + ext.merge_requests.sort_by(&:iid) end # All branches containing the current issue's ID, except for @@ -163,9 +138,13 @@ class Issue < ActiveRecord::Base def closed_by_merge_requests(current_user = nil) return [] unless open? - notes.system.flat_map do |note| - note.all_references(current_user).merge_requests - end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } + ext = all_references(current_user) + + notes.system.each do |note| + note.all_references(current_user, extractor: ext) + end + + ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) } end def moved? diff --git a/app/models/key.rb b/app/models/key.rb index b2b57849f8a..0532e84f47d 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: keys -# -# id :integer not null, primary key -# user_id :integer -# created_at :datetime -# updated_at :datetime -# key :text -# title :string -# type :string -# fingerprint :string -# public :boolean default(FALSE), not null -# - require 'digest/md5' class Key < ActiveRecord::Base @@ -41,7 +26,7 @@ class Key < ActiveRecord::Base end def publishable_key - #Removes anything beyond the keytype and key itself + # Removes anything beyond the keytype and key itself self.key.split[0..1].join(' ') end diff --git a/app/models/label.rb b/app/models/label.rb index 9a22398d952..e5ad11983be 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: labels -# -# id :integer not null, primary key -# title :string -# color :string -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) -# description :string -# - class Label < ActiveRecord::Base include Referable include Subscribable @@ -117,6 +103,10 @@ class Label < ActiveRecord::Base LabelsHelper::text_color_for_bg(self.color) end + def title=(value) + write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? + end + private def label_format_reference(format = :id) diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 7b8e872b6dd..47bd6eaf35f 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: label_links -# -# id :integer not null, primary key -# label_id :integer -# target_id :integer -# target_type :string -# created_at :datetime -# updated_at :datetime -# - class LabelLink < ActiveRecord::Base belongs_to :target, polymorphic: true belongs_to :label diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb new file mode 100644 index 00000000000..bbefc911b29 --- /dev/null +++ b/app/models/legacy_diff_note.rb @@ -0,0 +1,157 @@ +class LegacyDiffNote < Note + serialize :st_diff + + validates :line_code, presence: true, line_code: true + + before_create :set_diff + + class << self + def build_discussion_id(noteable_type, noteable_id, line_code, active = true) + [super(noteable_type, noteable_id), line_code, active].join("-") + end + end + + def diff_note? + true + end + + def legacy_diff_note? + true + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) + end + + def diff_file_hash + line_code.split('_')[0] if line_code + end + + def diff_old_line + line_code.split('_')[1].to_i if line_code + end + + def diff_new_line + line_code.split('_')[2].to_i if line_code + end + + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def diff_file_path + diff.new_path.presence || diff.old_path + end + + def diff_lines + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + end + + def diff_line + @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + end + + def diff_line_text + diff_line.try(:text) + end + + def diff_line_type + diff_line.try(:type) + end + + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight + end + + def truncated_diff_lines + max_number_of_lines = 16 + prev_match_line = nil + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.type == "match" + prev_lines.clear + prev_match_line = line + else + prev_lines << line + + break if generate_line_code(line) == self.line_code + + prev_lines.shift if prev_lines.length >= max_number_of_lines + end + end + + prev_lines + end + + # Check if this note is part of an "active" discussion + # + # This will always return true for anything except MergeRequest noteables, + # which have special logic. + # + # If the note's current diff cannot be matched in the MergeRequest's current + # diff, it's considered inactive. + def active? + return @active if defined?(@active) + return true if for_commit? + return true unless self.diff + return false unless noteable + + noteable_diff = find_noteable_diff + + if noteable_diff + parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) + + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + else + @active = false + end + + @active + end + + private + + def find_diff + return nil unless noteable + return @diff if defined?(@diff) + + @diff = noteable.diffs(Commit.max_diff_options).find do |d| + d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash + end + end + + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = diff_for_line_code unless for_merge_request? + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff_for_line_code + attributes = { + noteable_type: noteable_type, + line_code: line_code + } + + if for_commit? + attributes[:commit_id] = commit_id + else + attributes[:noteable_id] = noteable_id + end + + self.class.where(attributes).last.try(:diff) + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) + end + + # Find the diff on noteable that matches our own + def find_noteable_diff + diffs = noteable.diffs(Commit.max_diff_options) + diffs.find { |d| d.new_path == self.diff.new_path } + end +end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 927e764af92..18657c3e1c8 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects -# -# id :integer not null, primary key -# oid :string not null -# size :integer not null -# created_at :datetime -# updated_at :datetime -# file :string -# - class LfsObject < ActiveRecord::Base has_many :lfs_objects_projects, dependent: :destroy has_many :projects, through: :lfs_objects_projects diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 890736bfc80..0fd5f089db9 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects_projects -# -# id :integer not null, primary key -# lfs_object_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class LfsObjectsProject < ActiveRecord::Base belongs_to :project belongs_to :lfs_object diff --git a/app/models/member.rb b/app/models/member.rb index cca82da89f1..d3060f07fc0 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string not null -# user_id :integer -# notification_level :integer not null -# type :string -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string -# invite_token :string -# invite_accepted_at :datetime -# - class Member < ActiveRecord::Base include Sortable include Gitlab::Access diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index a48c1943e6f..f63a0debf1a 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string not null -# user_id :integer -# notification_level :integer not null -# type :string -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string -# invite_token :string -# invite_accepted_at :datetime -# - class GroupMember < Member SOURCE_TYPE = 'Namespace' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 143350a0b55..46955b430f3 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string not null -# user_id :integer -# notification_level :integer not null -# type :string -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string -# invite_token :string -# invite_accepted_at :datetime -# - class ProjectMember < Member SOURCE_TYPE = 'Project' @@ -24,7 +5,6 @@ class ProjectMember < Member belongs_to :project, class_name: 'Project', foreign_key: 'source_id' - # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE validates_format_of :source_type, with: /\AProject\z/ @@ -34,6 +14,8 @@ class ProjectMember < Member scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) } scope :with_user, ->(user) { where(user_id: user.id) } + before_destroy :delete_member_todos + class << self # Add users to project teams with passed access option @@ -121,6 +103,10 @@ class ProjectMember < Member private + def delete_member_todos + user.todos.where(project_id: source_id).destroy_all if user + end + def send_invite notification_service.invite_project_member(self, @raw_invite_token) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4175e1e5fba..722c258244c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1,33 +1,3 @@ -# == Schema Information -# -# Table name: merge_requests -# -# id :integer not null, primary key -# target_branch :string not null -# source_branch :string not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string -# merge_status :string -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string -# merge_params :text -# merge_when_build_succeeds :boolean default(FALSE), not null -# merge_user_id :integer -# merge_commit_sha :string -# deleted_at :datetime -# - class MergeRequest < ActiveRecord::Base include InternalId include Issuable @@ -56,6 +26,10 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare + # Temporary fields to store target_sha, and base_sha to + # compare when importing pull requests from GitHub + attr_accessor :base_target_sha, :head_source_sha + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -312,6 +286,18 @@ class MergeRequest < ActiveRecord::Base last_commit == source_project.commit(source_branch) end + def should_remove_source_branch? + merge_params['should_remove_source_branch'].present? + end + + def force_remove_source_branch? + merge_params['force_remove_source_branch'].present? + end + + def remove_source_branch? + should_remove_source_branch? || force_remove_source_branch? + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 @@ -452,7 +438,10 @@ class MergeRequest < ActiveRecord::Base self.merge_when_build_succeeds = false self.merge_user = nil - self.merge_params = nil + if merge_params + merge_params.delete('should_remove_source_branch') + merge_params.delete('commit_message') + end self.save end @@ -520,10 +509,14 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).try(:sha) + return @base_target_sha if defined?(@base_target_sha) + + target_project.repository.commit(target_branch).try(:sha) end def source_sha + return @head_source_sha if defined?(@head_source_sha) + last_commit.try(:sha) || source_tip.try(:sha) end @@ -544,7 +537,7 @@ class MergeRequest < ActiveRecord::Base end def ref_is_fetched? - File.exists?(File.join(project.repository.path_to_repo, ref_path)) + File.exist?(File.join(project.repository.path_to_repo, ref_path)) end def ensure_ref_fetched diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 8951e92a0b8..7d5103748f5 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: merge_request_diffs -# -# id :integer not null, primary key -# state :string -# st_commits :text -# st_diffs :text -# merge_request_id :integer not null -# created_at :datetime -# updated_at :datetime -# base_commit_sha :string -# real_size :string -# - class MergeRequestDiff < ActiveRecord::Base include Sortable @@ -21,7 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -53,8 +38,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha, + self.base, + self.head, ) compare.diffs(options) end @@ -113,9 +98,7 @@ class MergeRequestDiff < ActiveRecord::Base commits = compare.commits if commits.present? - commits = Commit.decorate(commits, merge_request.source_project). - sort_by(&:created_at). - reverse + commits = Commit.decorate(commits, merge_request.source_project).reverse end commits @@ -159,7 +142,7 @@ class MergeRequestDiff < ActiveRecord::Base self.st_diffs = new_diffs - self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch) + self.base_commit_sha = self.repository.merge_base(self.head, self.base) self.save end @@ -175,10 +158,24 @@ class MergeRequestDiff < ActiveRecord::Base end def source_sha + return head_source_sha if head_source_sha.present? + source_commit = merge_request.source_project.commit(source_branch) source_commit.try(:sha) end + def target_sha + merge_request.target_sha + end + + def base + self.target_sha || self.target_branch + end + + def head + self.source_sha + end + def compare @compare ||= begin @@ -187,8 +184,8 @@ class MergeRequestDiff < ActiveRecord::Base Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha + self.base, + self.head ) end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 5ee8a965ad8..e0c8454a998 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: milestones -# -# id :integer not null, primary key -# title :string not null -# project_id :integer not null -# description :text -# due_date :date -# created_at :datetime -# updated_at :datetime -# state :string -# iid :integer -# - class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. @@ -74,25 +59,67 @@ class Milestone < ActiveRecord::Base end end + def self.reference_prefix + '%' + end + def self.reference_pattern - nil + # NOTE: The iid pattern only matches when all characters on the expression + # are digits, so it will match %2 but not %2.1 because that's probably a + # milestone name and we want it to be matched as such. + @reference_pattern ||= %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)} + (?: + (? + \d+(?!\S\w)\b # Integer-based milestone iid, or + ) | + (? + [^"\s]+\b | # String-based single-word milestone title, or + "[^"]+" # String-based multi-word milestone surrounded in quotes + ) + ) + }x end def self.link_reference_pattern @link_reference_pattern ||= super("milestones", /(?\d+)/) end - def self.upcoming - self.where('due_date > ?', Time.now).reorder(due_date: :asc).first - end + def self.upcoming_ids_by_projects(projects) + rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) - def to_reference(from_project = nil) - escaped_title = self.title.gsub("]", "\\]") + if Gitlab::Database.postgresql? + rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') + else + rel. + group(:project_id). + having('due_date = MIN(due_date)'). + pluck(:id, :project_id, :due_date). + map(&:first) + end + end - h = Gitlab::Routing.url_helpers - url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) + ## + # Returns the String necessary to reference this Milestone in Markdown + # + # format - Symbol format to use (default: :iid, optional: :name) + # + # Examples: + # + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # + def to_reference(from_project = nil, format: :iid) + format_reference = milestone_format_reference(format) + reference = "#{self.class.reference_prefix}#{format_reference}" - "[#{escaped_title}](#{url})" + if cross_project_reference?(from_project) + project.to_reference + reference + else + reference + end end def reference_link_text(from_project = nil) @@ -129,6 +156,10 @@ class Milestone < ActiveRecord::Base nil end + def title=(value) + write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? + end + # Sorts the issues for the given IDs. # # This method runs a single SQL query using a CASE statement to update the @@ -160,4 +191,16 @@ class Milestone < ActiveRecord::Base issues.where(id: ids). update_all(["position = CASE #{conditions} ELSE position END", *pairs]) end + + private + + def milestone_format_reference(format = :iid) + raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) + + if format == :name && !name.include?('"') + %("#{name}") + else + iid + end + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 741e912171d..da19462f265 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,20 +1,3 @@ -# == Schema Information -# -# Table name: namespaces -# -# id :integer not null, primary key -# name :string not null -# path :string not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string -# description :string default(""), not null -# avatar :string -# share_with_group_lock :boolean default(FALSE) -# visibility_level :integer default(20), not null -# - class Namespace < ActiveRecord::Base include Sortable include Gitlab::ShellAdapter @@ -127,6 +110,10 @@ class Namespace < ActiveRecord::Base # Ensure old directory exists before moving it gitlab_shell.add_namespace(path_was) + if any_project_has_container_registry_tags? + raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + end + if gitlab_shell.mv_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) @@ -148,6 +135,10 @@ class Namespace < ActiveRecord::Base end end + def any_project_has_container_registry_tags? + projects.any?(&:has_container_registry_tags?) + end + def send_update_instructions projects.each do |project| project.send_move_instructions("#{path_was}/#{project.path}") diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index f4e90125373..a2aee2f925b 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -22,9 +22,16 @@ module Network def collect_notes h = Hash.new(0) - @project.notes.where('noteable_type = ?' ,"Commit").group('notes.commit_id').select('notes.commit_id, count(notes.id) as note_count').each do |item| - h[item.commit_id] = item.note_count.to_i - end + + @project + .notes + .where('noteable_type = ?', 'Commit') + .group('notes.commit_id') + .select('notes.commit_id, count(notes.id) as note_count') + .each do |item| + h[item.commit_id] = item.note_count.to_i + end + h end @@ -89,7 +96,7 @@ module Network end end - if self.class.max_count / 2 < offset then + if self.class.max_count / 2 < offset # get max index that commit is displayed in the center. offset - self.class.max_count / 2 else @@ -130,7 +137,7 @@ module Network commit.parents(@map).each do |parent| range = commit.time..parent.time - space = if commit.space >= parent.space then + space = if commit.space >= parent.space find_free_parent_space(range, parent.space, -1, commit.space) else find_free_parent_space(range, commit.space, -1, parent.space) @@ -144,7 +151,7 @@ module Network end def find_free_parent_space(range, space_base, space_step, space_default) - if is_overlap?(range, space_default) then + if is_overlap?(range, space_default) find_free_space(range, space_step, space_base, space_default) else space_default @@ -155,9 +162,9 @@ module Network range.each do |i| if i != range.first && i != range.last && - @commits[i].spaces.include?(overlap_space) then + @commits[i].spaces.include?(overlap_space) - return true; + return true end end @@ -198,7 +205,7 @@ module Network # Visit branching chains leaves.each do |l| parents = l.parents(@map).select{|p| p.space.zero?} - for p in parents + parents.each do |p| place_chain(p, l.time) end end @@ -216,7 +223,7 @@ module Network end def mark_reserved(time_range, space) - for day in time_range + time_range.each do |day| @reserved[day].push(space) end end @@ -225,15 +232,15 @@ module Network space_default ||= space_base reserved = [] - for day in time_range + time_range.each do |day| reserved.push(*@reserved[day]) end reserved.uniq! space = space_default - while reserved.include?(space) do + while reserved.include?(space) space += space_step - if space < space_base then + if space < space_base space_step *= -1 space = space_base + space_step end @@ -253,7 +260,7 @@ module Network leaves = [] leaves.push(commit) if commit.space.zero? - while true + loop do return leaves if commit.parents(@map).count.zero? commit = commit.parents(@map).first diff --git a/app/models/note.rb b/app/models/note.rb index deee2b9e885..c21981ead84 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -1,34 +1,12 @@ -# == Schema Information -# -# Table name: notes -# -# id :integer not null, primary key -# note :text -# noteable_type :string -# author_id :integer -# created_at :datetime -# updated_at :datetime -# project_id :integer -# attachment :string -# line_code :string -# commit_id :string -# noteable_id :integer -# system :boolean default(FALSE), not null -# st_diff :text -# updated_by_id :integer -# is_award :boolean default(FALSE), not null -# - -require 'carrierwave/orm/activerecord' - class Note < ActiveRecord::Base + extend ActiveModel::Naming include Gitlab::CurrentSettings include Participable include Mentionable default_value_for :system, false - attr_mentionable :note, cache: true, pipeline: :note + attr_mentionable :note, pipeline: :note participant :author belongs_to :project @@ -41,29 +19,33 @@ class Note < ActiveRecord::Base delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true + delegate :title, to: :noteable, allow_nil: true before_validation :set_award! - before_validation :clear_blank_line_code! validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } - validates :line_code, line_code: true, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } - validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } - validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } + validates :noteable_type, presence: true + validates :noteable_id, presence: true, unless: :for_commit? + validates :commit_id, presence: true, if: :for_commit? validates :author, presence: true + validate unless: :for_commit? do |note| + unless note.noteable.try(:project) == note.project + errors.add(:invalid_project, 'Note and noteable project mismatch') + end + end + mount_uploader :attachment, AttachmentUploader # Scopes scope :awards, ->{ where(is_award: true) } scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } - scope :inline, ->{ where("line_code IS NOT NULL") } - scope :not_inline, ->{ where(line_code: nil) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } @@ -71,52 +53,61 @@ class Note < ActiveRecord::Base scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } + scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :non_diff_notes, ->{ where(type: ['Note', nil]) } + scope :with_associations, -> do includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end - serialize :st_diff - before_create :set_diff, if: ->(n) { n.line_code.present? } + before_validation :clear_blank_line_code! class << self - def discussions_from_notes(notes) - discussion_ids = [] - discussions = [] - - notes.each do |note| - next if discussion_ids.include?(note.discussion_id) - - # don't group notes for the main target - if !note.for_diff_line? && note.for_merge_request? - discussions << [note] - else - discussions << notes.select do |other_note| - note.discussion_id == other_note.discussion_id - end - discussion_ids << note.discussion_id - end - end + def model_name + ActiveModel::Name.new(self, nil, 'note') + end + + def build_discussion_id(noteable_type, noteable_id) + [:discussion, noteable_type.try(:underscore), noteable_id].join("-") + end - discussions + def discussions + all.group_by(&:discussion_id).values end - def build_discussion_id(type, id, line_code) - [:discussion, type.try(:underscore), id, line_code].join("-").to_sym + def grouped_diff_notes + legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. # - # query - The search query as a String. + # query - The search query as a String. + # as_user - Limit results to those viewable by a specific user # # Returns an ActiveRecord::Relation. - def search(query) + def search(query, as_user: nil) table = arel_table pattern = "%#{query}%" - where(table[:note].matches(pattern)) + found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id'). + where(table[:note].matches(pattern)) + + if as_user + found_notes.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential IS TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: as_user.id, + project_ids: as_user.authorized_projects.select(:id)) + else + found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') + end end def grouped_awards @@ -137,167 +128,39 @@ class Note < ActiveRecord::Base system && SystemNoteService.cross_reference?(note) end - def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i - end - - def find_diff - return nil unless noteable - return @diff if defined?(@diff) - - # Don't use ||= because nil is a valid value for @diff - @diff = noteable.diffs(Commit.max_diff_options).find do |d| - Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path - end + def diff_note? + false end - def hook_attrs - attributes + def legacy_diff_note? + false end - def set_diff - # First lets find notes with same diff - # before iterating over all mr diffs - diff = diff_for_line_code unless for_merge_request? - diff ||= find_diff - - self.st_diff = diff.to_hash if diff - end - - def diff - @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) - end - - def diff_for_line_code - Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) - end - - # Check if this note is part of an "active" discussion - # - # This will always return true for anything except MergeRequest noteables, - # which have special logic. - # - # If the note's current diff cannot be matched in the MergeRequest's current - # diff, it's considered inactive. def active? - return true unless self.diff - return false unless noteable - return @active if defined?(@active) - - noteable_diff = find_noteable_diff - - if noteable_diff - parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } - else - @active = false - end - - @active - end - - def diff_file_index - line_code.split('_')[0] if line_code - end - - def diff_file_name - diff.new_path if diff - end - - def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end - end - - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code + true end - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - - def diff_line - return @diff_line if @diff_line - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line = line.text - end - end - end - - @diff_line - end - - def diff_line_type - return @diff_line_type if @diff_line_type - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line_type = line.type - end - end - end - - @diff_line_type - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line + def discussion_id + @discussion_id ||= + if for_merge_request? + [:discussion, :note, id].join("-") else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines + self.class.build_discussion_id(noteable_type, noteable_id || commit_id) end - end - - prev_lines end - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i end - def discussion_id - @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) + def hook_attrs + attributes end def for_commit? noteable_type == "Commit" end - def for_commit_diff_line? - for_commit? && for_diff_line? - end - - def for_diff_line? - line_code.present? - end - def for_issue? noteable_type == "Issue" end @@ -306,10 +169,6 @@ class Note < ActiveRecord::Base noteable_type == "MergeRequest" end - def for_merge_request_diff_line? - for_merge_request? && for_diff_line? - end - def for_snippet? noteable_type == "Snippet" end @@ -382,14 +241,8 @@ class Note < ActiveRecord::Base self.line_code = nil if self.line_code.blank? end - # Find the diff on noteable that matches our own - def find_noteable_diff - diffs = noteable.diffs(Commit.max_diff_options) - diffs.find { |d| d.new_path == self.diff.new_path } - end - def awards_supported? - (for_issue? || for_merge_request?) && !for_diff_line? + (for_issue? || for_merge_request?) && !diff_note? end def contains_emoji_only? diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 846773752a6..5001738f411 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: notification_settings -# -# id :integer not null, primary key -# user_id :integer not null -# source_id :integer not null -# source_type :string not null -# level :integer default(0), not null -# created_at :datetime not null -# updated_at :datetime not null -# - class NotificationSetting < ActiveRecord::Base enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index c78c7f4aa0e..116fb71ac08 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: oauth_access_tokens -# -# id :integer not null, primary key -# resource_owner_id :integer -# application_id :integer -# token :string not null -# refresh_token :string -# expires_in :integer -# revoked_at :datetime -# created_at :datetime not null -# scopes :string -# - class OauthAccessToken < ActiveRecord::Base belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 1d5f4c50254..82c1c4de3a0 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -1,18 +1,2 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string -# type :string -# visibility_level :integer default(0), not null -# - class PersonalSnippet < Snippet end diff --git a/app/models/project.rb b/app/models/project.rb index fdbabc7cf71..74db163bcb5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,48 +1,3 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string -# path :string -# description :text -# created_at :datetime -# updated_at :datetime -# creator_id :integer -# issues_enabled :boolean default(TRUE), not null -# merge_requests_enabled :boolean default(TRUE), not null -# wiki_enabled :boolean default(TRUE), not null -# namespace_id :integer -# issues_tracker :string default("gitlab"), not null -# issues_tracker_id :string -# snippets_enabled :boolean default(TRUE), not null -# last_activity_at :datetime -# import_url :string -# visibility_level :integer default(0), not null -# archived :boolean default(FALSE), not null -# avatar :string -# import_status :string -# repository_size :float default(0.0) -# star_count :integer default(0), not null -# import_type :string -# import_source :string -# commit_count :integer default(0) -# import_error :text -# ci_id :integer -# builds_enabled :boolean default(TRUE), not null -# shared_runners_enabled :boolean default(TRUE), not null -# runners_token :string -# build_coverage_regex :string -# build_allow_git_fetch :boolean default(TRUE), not null -# build_timeout :integer default(3600), not null -# pending_delete :boolean default(FALSE) -# public_builds :boolean default(TRUE), not null -# main_language :string -# pushes_since_gc :integer default(0) -# last_repository_check_failed :boolean -# last_repository_check_at :datetime -# - require 'carrierwave/orm/activerecord' class Project < ActiveRecord::Base @@ -67,6 +22,7 @@ class Project < ActiveRecord::Base default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -94,6 +50,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + alias_attribute :title, :name + # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' @@ -213,17 +171,17 @@ class Project < ActiveRecord::Base scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } - scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } - scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } - scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } - scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } - scope :in_group_namespace, -> { joins(:group) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } + scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) } scope :non_archived, -> { where(archived: false) } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } + scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + + scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } + scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } state_machine :import_status, initial: :none do event :import_start do @@ -246,23 +204,10 @@ class Project < ActiveRecord::Base state :finished state :failed - after_transition any => :started, do: :schedule_add_import_job - after_transition any => :finished, do: :clear_import_data + after_transition any => :finished, do: :reset_cache_and_import_attrs end class << self - def abandoned - where('projects.last_activity_at < ?', 6.months.ago) - end - - def with_push - joins(:events).where('events.action = ?', Event::PUSHED) - end - - def active - joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') - end - # Searches for a list of projects based on the query given in `query`. # # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive @@ -324,10 +269,6 @@ class Project < ActiveRecord::Base projects.iwhere('projects.path' => project_path).take end - def find_by_ci_id(id) - find_by(ci_id: id.to_i) - end - def visibility_levels Gitlab::VisibilityLevel.options end @@ -358,10 +299,6 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end - - def visible_to_user(user) - where(id: user.authorized_projects.select(:id).reorder(nil)) - end end def team @@ -372,6 +309,34 @@ 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_repository + return unless Gitlab.config.registry.enabled + + @container_registry_repository ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace) + 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) + end + end + + def container_registry_repository_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_registry_repository + + container_registry_repository.tags.any? + end + def commit(id = 'HEAD') repository.commit(id) end @@ -385,10 +350,6 @@ class Project < ActiveRecord::Base id && persisted? end - def schedule_add_import_job - run_after_commit(:add_import_job) - end - def add_import_job if forked? job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) @@ -403,7 +364,7 @@ class Project < ActiveRecord::Base end end - def clear_import_data + def reset_cache_and_import_attrs update(import_error: nil) ProjectCacheWorker.perform_async(self.id) @@ -412,14 +373,14 @@ class Project < ActiveRecord::Base end def import_url=(value) - import_url = Gitlab::ImportUrl.new(value) + import_url = Gitlab::UrlSanitizer.new(value) create_or_update_import_data(credentials: import_url.credentials) super(import_url.sanitized_url) end def import_url if import_data && super - import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials) + import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials) import_url.full_url else super @@ -469,17 +430,18 @@ class Project < ActiveRecord::Base end def safe_import_url - result = URI.parse(self.import_url) - result.password = '*****' unless result.password.nil? - result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user - result.to_s - rescue - self.import_url + Gitlab::UrlSanitizer.new(import_url).masked_url end def check_limit unless creator.can_create_project? or namespace.kind == 'group' - self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it") + projects_limit = creator.projects_limit + + if projects_limit == 0 + self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions") + else + self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it") + end end rescue self.errors.add(:base, "Can't check your ability to create project") @@ -787,6 +749,11 @@ class Project < ActiveRecord::Base expire_caches_before_rename(old_path_with_namespace) + if has_container_registry_tags? + # we currently doesn't support renaming repository if it contains tags in container registry + raise Exception.new('Project cannot be renamed, because tags are present in its container registry') + end + if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) # If repository moved successfully we need to send update instructions to users. # However we cannot allow rollback since we moved repository @@ -983,13 +950,13 @@ class Project < ActiveRecord::Base shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) end - def valid_runners_token? token + def valid_runners_token?(token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end # TODO (ayufan): For now we use runners_token (backward compatibility) # In 8.4 every build will have its own individual token valid for time of build - def valid_build_token? token + def valid_build_token?(token) self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end @@ -1045,6 +1012,24 @@ class Project < ActiveRecord::Base update_attribute(:pending_delete, true) end + def running_or_pending_build_count(force: false) + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do + builds.running_or_pending.count(:all) + end + end + + def mark_import_as_failed(error_message) + original_errors = errors.dup + sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) + + import_fail + update_column(:import_error, sanitized_message) + rescue ActiveRecord::ActiveRecordError => e + Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") + ensure + @errors = original_errors + end + def add_export_job(current_user_id:) job_id = ProjectExportWorker.perform_async(current_user_id, self.id) diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 66f5a609bf5..e52a6bd7c84 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: project_group_links -# -# id :integer not null, primary key -# project_id :integer not null -# group_id :integer not null -# created_at :datetime -# updated_at :datetime -# group_access :integer default(30), not null -# - class ProjectGroupLink < ActiveRecord::Base GUEST = 10 REPORTER = 20 diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 7830f764ed3..ca8a9b4217b 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: project_import_data -# -# id :integer not null, primary key -# project_id :integer -# data :text -# encrypted_credentials :text -# encrypted_credentials_iv :string -# encrypted_credentials_salt :string -# - require 'carrierwave/orm/activerecord' class ProjectImportData < ActiveRecord::Base @@ -18,7 +6,8 @@ class ProjectImportData < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, marshal: true, encode: true, - mode: :per_attribute_iv_and_salt + mode: :per_attribute_iv_and_salt, + algorithm: 'aes-256-cbc' serialize :data, JSON diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 368485a060a..7c23b766763 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index ffb7455b014..d839221d315 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class AssemblaService < Service include HTTParty diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index c36ee95e378..1d1780dcfbf 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class BambooService < CiService include HTTParty diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index f9f4897a065..86a06321e21 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require "addressable/uri" class BuildkiteService < CiService diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 20cdfcaffb2..54da4d74fc5 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class BuildsEmailService < Service prop_accessor :recipients boolean_accessor :add_pusher diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 28c969fe57f..511b2eac792 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class CampfireService < Service prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 9bc8f982da6..596c00705ad 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - # Base class for CI services # List methods you need to implement to get your CI service # working with GitLab Merge Requests diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 4d1319eb6f8..6b2b1daa724 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class CustomIssueTrackerService < IssueTrackerService prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index d8e00e018cc..966dbc41d73 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class DroneCiService < CiService prop_accessor :drone_url, :token, :enable_ssl_verification diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 2dbd29062df..e0083c43adb 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class EmailsOnPushService < Service prop_accessor :send_from_committer_email prop_accessor :disable_diffs diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 5469049bb5e..d7b6e505191 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class ExternalWikiService < Service include HTTParty @@ -49,7 +25,7 @@ class ExternalWikiService < Service def execute(_data) @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil - if @response !=200 + if @response != 200 nil end end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 3dc1e0fbe8b..dd00275187f 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require "flowdock-git-hook" class FlowdockService < Service diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index b4c311cf664..598aca5e06d 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require "gemnasium/gitlab_service" class GemnasiumService < Service diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index a92f7226083..bbc312f5215 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - # TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed class GitlabCiService < CiService # We override the active accessor to always make GitLabCiService disabled diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 1adaeeb3b2b..5d17c358330 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing.url_helpers diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index f9ddf588722..0ff4f4c8dd2 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class HipchatService < Service MAX_COMMITS = 3 diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index b9a592d7096..2e5e854fc5e 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require 'uri' class IrkerService < Service @@ -94,7 +70,7 @@ class IrkerService < Service private def get_channels - return true unless :activated? + return true unless activated? return true if recipients.nil? || recipients.empty? map_recipients diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 98a3a7c6b86..6ae9b16d3ce 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class IssueTrackerService < Service validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index ba68658f0bd..beda89d3963 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class JiraService < IssueTrackerService include HTTParty include Gitlab::Routing.url_helpers diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index acaa0c39365..ad19b7795da 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class PivotaltrackerService < Service include HTTParty diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index a640c8cb440..3dd878e4c7d 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class PushoverService < Service include HTTParty base_uri 'https://api.pushover.net/1' diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index e2137e92c62..11cce3e0561 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class RedmineService < IssueTrackerService prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 83ffa53a407..cf9e4d5a8b6 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class SlackService < Service prop_accessor :webhook, :username, :channel boolean_accessor :notify_only_broken_builds diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb index c124cad4afd..69c21b3fc38 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/slack_service/build_message.rb @@ -35,8 +35,8 @@ class SlackService private def message - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)" - end + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + end def format(string) Slack::Notifier::LinkFormatter.format(string) diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 438ff33fdff..88e053ec192 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -34,7 +34,12 @@ class SlackService private def message - "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" + case state + when "opened" + "[#{project_link}] Issue #{state} by #{user_name}" + else + "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + end end def opened_issue? @@ -42,7 +47,11 @@ class SlackService end def description_message - [{ text: format(description), color: attachment_color }] + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: "#C95823" }] end def project_link @@ -50,7 +59,11 @@ class SlackService end def issue_link - "[issue ##{issue_iid}](#{issue_url})" + "[#{issue_title}](#{issue_url})" + end + + def issue_title + "##{issue_iid} #{title}" end end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 4015da31509..b0dcb52eba1 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class TeamcityService < CiService include HTTParty diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index b4b2807eba4..25b5d777641 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string -# type :string -# visibility_level :integer default(0), not null -# - class ProjectSnippet < Snippet belongs_to :project belongs_to :author, class_name: "User" @@ -23,5 +7,6 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } - participant :author, :notes + participant :author + participant :notes_with_associations end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 7c1a61bb0bf..25d82929c0b 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -27,6 +27,10 @@ class ProjectWiki @project.path_with_namespace + ".wiki" end + def web_url + Gitlab::Routing.url_helpers.namespace_project_wiki_url(@project.namespace, @project, :home) + end + def url_to_repo gitlab_shell.url_to_repo(path_with_namespace) end @@ -40,7 +44,7 @@ class ProjectWiki end def wiki_base_path - ["/", @project.path_with_namespace, "/wikis"].join('') + [Gitlab.config.gitlab.relative_url_root, "/", @project.path_with_namespace, "/wikis"].join('') end # Returns the Gollum::Wiki object. @@ -113,7 +117,7 @@ class ProjectWiki end def page_title_and_dir(title) - title_array = title.split("/") + title_array = title.split("/") title = title_array.pop [title, title_array.join("/")] end @@ -142,6 +146,16 @@ class ProjectWiki wiki end + def hook_attrs + { + web_url: web_url, + git_ssh_url: ssh_url_to_repo, + git_http_url: http_url_to_repo, + path_with_namespace: path_with_namespace, + default_branch: default_branch + } + end + private def init_repo(path_with_namespace) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 3d2052c892c..33cf046fa75 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: protected_branches -# -# id :integer not null, primary key -# project_id :integer not null -# name :string not null -# created_at :datetime -# updated_at :datetime -# developers_can_push :boolean default(FALSE), not null -# - class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter diff --git a/app/models/release.rb b/app/models/release.rb index dc700d1ea5a..e196b84eb18 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: releases -# -# id :integer not null, primary key -# tag :string -# description :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# - class Release < ActiveRecord::Base belongs_to :project diff --git a/app/models/repository.rb b/app/models/repository.rb index 7aebfe279fb..1ab163510bf 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -81,7 +81,7 @@ class Repository def commit(id = 'HEAD') return nil unless exists? commit = Gitlab::Git::Commit.find(raw_repository, id) - commit = Commit.new(commit, @project) if commit + commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError nil @@ -195,6 +195,10 @@ class Repository cache.fetch(:branch_names) { branches.map(&:name) } end + def branch_exists?(branch_name) + branch_names.include?(branch_name) + end + def tag_names cache.fetch(:tag_names) { raw_repository.tag_names } end @@ -241,7 +245,7 @@ class Repository def cache_keys %i(size branch_names tag_names commit_count readme version contribution_guide changelog - license_blob license_key) + license_blob license_key gitignore) end def build_cache @@ -252,6 +256,10 @@ class Repository end end + def expire_gitignore + cache.expire(:gitignore) + end + def expire_tags_cache cache.expire(:tag_names) @tags = nil @@ -453,7 +461,7 @@ class Repository def version cache.fetch(:version) do tree(:head).blobs.find do |file| - file.name.downcase == 'version' + file.name.casecmp('version').zero? end end end @@ -468,33 +476,37 @@ class Repository def changelog cache.fetch(:changelog) do - tree(:head).blobs.find do |file| - file.name =~ /\A(changelog|history|changes|news)/i - end + file_on_head(/\A(changelog|history|changes|news)/i) end end def license_blob - return nil if !exists? || empty? + return nil unless head_exists? cache.fetch(:license_blob) do - tree(:head).blobs.find do |file| - file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i - end + file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i) end end def license_key - return nil if !exists? || empty? + return nil unless head_exists? cache.fetch(:license_key) do Licensee.license(path).try(:key) end end - def gitlab_ci_yml + def gitignore return nil if !exists? || empty? + cache.fetch(:gitignore) do + file_on_head(/\A\.gitignore\z/) + end + end + + def gitlab_ci_yml + return nil unless head_exists? + @gitlab_ci_yml ||= tree(:head).blobs.find do |file| file.name == '.gitlab-ci.yml' end @@ -795,7 +807,7 @@ class Repository def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + args << { mainline: 1 } if commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -809,7 +821,7 @@ class Repository def check_cherry_pick_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + args << 1 if commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? @@ -850,7 +862,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end @@ -960,12 +972,6 @@ class Repository end end - def main_language - return if empty? || rugged.head_unborn? - - Linguist::Repository.new(rugged, rugged.head.target_id).language - end - def avatar return nil unless exists? @@ -981,4 +987,12 @@ class Repository def cache @cache ||= RepositoryCache.new(path_with_namespace) end + + def head_exists? + exists? && !empty? && !rugged.head_unborn? + end + + def file_on_head(regex) + tree(:head).blobs.find { |file| file.name =~ regex } + end end diff --git a/app/models/security_event.rb b/app/models/security_event.rb index 0bee03974f1..d131c11cb6c 100644 --- a/app/models/security_event.rb +++ b/app/models/security_event.rb @@ -1,16 +1,2 @@ -# == Schema Information -# -# Table name: audit_events -# -# id :integer not null, primary key -# author_id :integer not null -# type :string not null -# entity_id :integer not null -# entity_type :string not null -# details :text -# created_at :datetime -# updated_at :datetime -# - class SecurityEvent < AuditEvent end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 99279a2e083..375f195dba7 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: sent_notifications -# -# id :integer not null, primary key -# project_id :integer -# noteable_id :integer -# noteable_type :string -# recipient_id :integer -# commit_id :string -# reply_key :string not null -# line_code :string -# - class SentNotification < ActiveRecord::Base belongs_to :project belongs_to :noteable, polymorphic: true diff --git a/app/models/service.rb b/app/models/service.rb index bf16a545307..de3fd24584a 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - # To add new service you should build a class inherited from Service # and implement a set of methods class Service < ActiveRecord::Base diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 2f905a90942..407697b745c 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string -# type :string -# visibility_level :integer default(0), not null -# - class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel include Linguist::BlobHelper @@ -46,7 +30,8 @@ class Snippet < ActiveRecord::Base scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } - participant :author, :notes + participant :author + participant :notes_with_associations def self.reference_prefix '$' @@ -116,6 +101,10 @@ class Snippet < ActiveRecord::Base content.lines.count > 1000 end + def notes_with_associations + notes.includes(:author, :project) + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb index f49eb7d88e2..12df68ef83b 100644 --- a/app/models/spam_log.rb +++ b/app/models/spam_log.rb @@ -1,20 +1,3 @@ -# == Schema Information -# -# Table name: spam_logs -# -# id :integer not null, primary key -# user_id :integer -# source_ip :string -# user_agent :string -# via_api :boolean -# project_id :integer -# noteable_type :string -# title :string -# description :text -# created_at :datetime not null -# updated_at :datetime not null -# - class SpamLog < ActiveRecord::Base belongs_to :user diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 242faa7d32e..3b8aa1eb866 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: subscriptions -# -# id :integer not null, primary key -# user_id :integer -# subscribable_id :integer -# subscribable_type :string -# subscribed :boolean -# created_at :datetime -# updated_at :datetime -# - class Subscription < ActiveRecord::Base belongs_to :user belongs_to :subscribable, polymorphic: true diff --git a/app/models/todo.rb b/app/models/todo.rb index d85f7bfdf57..3a091373329 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,24 +1,7 @@ -# == Schema Information -# -# Table name: todos -# -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# target_id :integer -# target_type :string not null -# author_id :integer -# action :integer not null -# state :string not null -# created_at :datetime -# updated_at :datetime -# note_id :integer -# commit_id :string -# - class Todo < ActiveRecord::Base - ASSIGNED = 1 - MENTIONED = 2 + ASSIGNED = 1 + MENTIONED = 2 + BUILD_FAILED = 3 belongs_to :author, class_name: "User" belongs_to :note @@ -46,6 +29,10 @@ class Todo < ActiveRecord::Base state :done end + def build_failed? + action == BUILD_FAILED + end + def body if note.present? note.note diff --git a/app/models/user.rb b/app/models/user.rb index 959b1f93758..172845c9d25 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,68 +1,3 @@ -# == Schema Information -# -# Table name: users -# -# id :integer not null, primary key -# email :string default(""), not null -# encrypted_password :string default(""), not null -# reset_password_token :string -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string -# last_sign_in_ip :string -# created_at :datetime -# updated_at :datetime -# name :string -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string default(""), not null -# linkedin :string default(""), not null -# twitter :string default(""), not null -# authentication_token :string -# theme_id :integer default(1), not null -# bio :string -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string -# confirmation_token :string -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string default(""), not null -# notification_email :string -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string -# encrypted_otp_secret :string -# encrypted_otp_secret_iv :string -# encrypted_otp_secret_salt :string -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) -# unlock_token :string -# otp_grace_period_started_at :datetime -# ldap_email :boolean default(FALSE), not null -# external :boolean default(FALSE) -# - require 'carrierwave/orm/activerecord' class User < ActiveRecord::Base @@ -85,14 +20,19 @@ class User < ActiveRecord::Base default_value_for :hide_no_password, false default_value_for :theme_id, gitlab_config.default_theme + attr_encrypted :otp_secret, + key: Gitlab::Application.config.secret_key_base, + mode: :per_attribute_iv_and_salt, + algorithm: 'aes-256-cbc' + devise :two_factor_authenticatable, - otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp + otp_secret_encryption_key: Gitlab::Application.config.secret_key_base alias_attribute :two_factor_enabled, :otp_required_for_login devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON - devise :lockable, :async, :recoverable, :rememberable, :trackable, + devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password @@ -177,6 +117,7 @@ class User < ActiveRecord::Base before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit + before_create :check_confirmation_email after_create :post_create_hook after_destroy :post_destroy_hook @@ -372,6 +313,10 @@ class User < ActiveRecord::Base @reset_token end + def check_confirmation_email + skip_confirmation! unless current_application_settings.send_user_confirmation_email + end + def recently_sent_password_reset? reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end @@ -446,17 +391,17 @@ class User < ActiveRecord::Base Project.where("projects.id IN (#{projects_union.to_sql})") end + def viewable_starred_projects + starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})", + [Project::PUBLIC, Project::INTERNAL]) + end + def owned_projects @owned_projects ||= Project.where('namespace_id IN (?) OR namespace_id = ?', owned_groups.select(:id), namespace.id).joins(:namespace) end - # Team membership in authorized projects - def tm_in_authorized_projects - ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id) - end - def is_admin? admin end @@ -546,10 +491,6 @@ class User < ActiveRecord::Base "#{name} (#{username})" end - def tm_of(project) - project.project_member_by_id(self.id) - end - def already_forked?(project) !!fork_of(project) end @@ -835,6 +776,23 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end + def assigned_open_merge_request_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do + assigned_merge_requests.opened.count + end + end + + def assigned_open_issues_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do + assigned_issues.opened.count + end + end + + def update_cache_counts + assigned_open_merge_request_count(force: true) + assigned_open_issues_count(force: true) + end + private def projects_union diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 413f3f485a8..0dfe597317e 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: users_star_projects -# -# id :integer not null, primary key -# project_id :integer not null -# user_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class UsersStarProject < ActiveRecord::Base belongs_to :project, counter_cache: :star_count, touch: true belongs_to :user diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb new file mode 100644 index 00000000000..e57b95f21ec --- /dev/null +++ b/app/services/auth/container_registry_authentication_service.rb @@ -0,0 +1,87 @@ +module Auth + class ContainerRegistryAuthenticationService < BaseService + include Gitlab::CurrentSettings + + AUDIENCE = 'container_registry' + + def execute + return error('not found', 404) unless registry.enabled + + unless current_user || project + return error('forbidden', 403) unless scope + end + + { token: authorized_token(scope).encoded } + end + + def self.full_access_token(*names) + registry = Gitlab.config.registry + token = JSONWebToken::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = AUDIENCE + token.expire_time = token_expire_at + token[:access] = names.map do |name| + { type: 'repository', name: name, actions: %w(*) } + end + token.encoded + end + + 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 = ContainerRegistryAuthenticationService.token_expire_at + token[:access] = accesses.compact + token + end + + def scope + return unless params[:scope] + + @scope ||= process_scope(params[:scope]) + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + return unless type == 'repository' + + process_repository_access(type, name, actions) + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + return false unless requested_project.container_registry_enabled? + + case requested_action + when 'pull' + requested_project == project || can?(current_user, :read_container_image, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_image, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + + def self.token_expire_at + Time.now + current_application_settings.container_registry_token_expire_delay.minutes + end + end +end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb new file mode 100644 index 00000000000..5bc0c31cb42 --- /dev/null +++ b/app/services/ci/create_pipeline_service.rb @@ -0,0 +1,50 @@ +module Ci + class CreatePipelineService < BaseService + def execute + pipeline = project.ci_commits.new(params) + + unless ref_names.include?(params[:ref]) + pipeline.errors.add(:base, 'Reference not found') + return pipeline + end + + unless commit + pipeline.errors.add(:base, 'Commit not found') + return pipeline + end + + unless can?(current_user, :create_pipeline, project) + pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline') + return pipeline + end + + begin + Ci::Commit.transaction do + pipeline.sha = commit.id + + unless pipeline.config_processor + pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') + raise ActiveRecord::Rollback + end + + pipeline.save! + pipeline.create_builds(current_user) + end + rescue + pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.') + end + + pipeline + end + + private + + def ref_names + @ref_names ||= project.repository.ref_names + end + + def commit + @commit ||= project.commit(params[:ref]) + end + end +end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 0d2aa1ff03d..5b6fefe669e 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -18,19 +18,16 @@ class CreateCommitBuildsService return false end - commit = project.ci_commit(sha, ref) - unless commit - commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag) + commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) - # Skip creating ci_commit when no gitlab-ci.yml is found - unless commit.ci_yaml_file - return false - end - - # Create a new ci_commit - commit.save! + # Skip creating ci_commit when no gitlab-ci.yml is found + unless commit.ci_yaml_file + return false end + # Create a new ci_commit + commit.save! + # Skip creating builds for commits that have [ci skip] unless commit.skip_ci? # Create builds for commit diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 66136b62617..a886f35981f 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -53,10 +53,6 @@ class GitPushService < BaseService # could cause the last commit of a merge request to change. update_merge_requests - # Checks if the main language has changed in the project and if so - # it updates it accordingly - update_main_language - perform_housekeeping end @@ -64,19 +60,6 @@ class GitPushService < BaseService @project.repository.copy_gitattributes(params[:ref]) end - def update_main_language - # Performance can be bad so for now only check main_language once - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937 - return if @project.main_language.present? - - return unless is_default_branch? - return unless push_to_new_branch? || push_to_existing_branch? - - current_language = @project.repository.main_language - @project.update_attributes(main_language: current_language) - true - end - protected def update_merge_requests diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 7410442609d..299a0a967b0 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -23,7 +23,7 @@ class GitTagPushService < BaseService commits = [] message = nil - if !Gitlab::Git.blank_ref?(params[:newrev]) + unless Gitlab::Git.blank_ref?(params[:newrev]) tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) if tag && tag.target == params[:newrev] diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3563cbaa997..c7d406cc331 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -24,6 +24,10 @@ module Issues todo_service.reassigned_issue(issue, current_user) end + if issue.previous_changes.include?('confidential') + create_confidentiality_note(issue) + end + added_labels = issue.labels - old_labels if added_labels.present? notification_service.relabeled_issue(issue, added_labels, current_user) @@ -37,5 +41,11 @@ module Issues def close_service Issues::CloseService end + + private + + def create_confidentiality_note(issue) + SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) + end end end diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb new file mode 100644 index 00000000000..566049525cb --- /dev/null +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -0,0 +1,17 @@ +module MergeRequests + class AddTodoWhenBuildFailsService < MergeRequests::BaseService + # Adds a todo to the parent merge_request when a CI build fails + def execute(commit_status) + each_merge_request(commit_status) do |merge_request| + todo_service.merge_request_build_failed(merge_request) + end + end + + # Closes any pending build failed todos for the parent MRs when a build is retried + def close(commit_status) + each_merge_request(commit_status) do |merge_request| + todo_service.merge_request_build_retried(merge_request) + end + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index e6837a18696..9d7fca6882d 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,5 +38,30 @@ module MergeRequests def filter_params super(:merge_request) end + + def merge_request_from(commit_status) + branches = commit_status.ref + + # This is for ref-less builds + branches ||= @project.repository.branch_names_contains(commit_status.sha) + + return [] if branches.blank? + + merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a + + merge_requests.uniq.select(&:source_project) + end + + def each_merge_request(commit_status) + merge_request_from(commit_status).each do |merge_request| + ci_commit = merge_request.ci_commit + + next unless ci_commit + next unless ci_commit.sha == commit_status.sha + + yield merge_request, ci_commit + end + end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index cd4230aa5e4..1b48899bb0a 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -66,7 +66,7 @@ module MergeRequests commits = merge_request.compare_commits if commits && commits.count == 1 commit = commits.first - merge_request.title = commit.title + merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) case issue diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 33609d01f20..96a25330af1 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -8,11 +8,14 @@ module MergeRequests @project = Project.find(params[:target_project_id]) if params[:target_project_id] filter_params - label_params = params[:label_ids] - merge_request = MergeRequest.new(params.except(:label_ids)) + label_params = params.delete(:label_ids) + force_remove_source_branch = params.delete(:force_remove_source_branch) + + merge_request = MergeRequest.new(params) merge_request.source_project = source_project merge_request.target_project ||= source_project merge_request.author = current_user + merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch if merge_request.save merge_request.update_attributes(label_ids: label_params) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 9a58383b398..9aaf5a5e561 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -45,10 +45,14 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) - if params[:should_remove_source_branch].present? - DeleteBranchService.new(@merge_request.source_project, current_user). + if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user). execute(merge_request.source_branch) end end + + def branch_deletion_user + @merge_request.force_remove_source_branch? ? @merge_request.author : current_user + end end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index d6af12f9739..8fd6a4ea1f6 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -20,15 +20,9 @@ module MergeRequests # Triggers the automatic merge of merge_request once the build succeeds def trigger(commit_status) - merge_requests = merge_request_from(commit_status) - - merge_requests.each do |merge_request| + each_merge_request(commit_status) do |merge_request, ci_commit| next unless merge_request.merge_when_build_succeeds? next unless merge_request.mergeable? - - ci_commit = merge_request.ci_commit - next unless ci_commit - next unless ci_commit.sha == commit_status.sha next unless ci_commit.success? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) @@ -47,20 +41,5 @@ module MergeRequests end end - private - - def merge_request_from(commit_status) - branches = commit_status.ref - - # This is for ref-less builds - branches ||= @project.repository.branch_names_contains(commit_status.sha) - - return [] if branches.blank? - - merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a - merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a - - merge_requests.uniq.select(&:source_project) - end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 8b3d56c2b4c..fe0579744b4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -12,6 +12,7 @@ module MergeRequests close_merge_requests reload_merge_requests reset_merge_when_build_succeeds + mark_pending_todos_done # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -80,6 +81,12 @@ module MergeRequests merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) end + def mark_pending_todos_done + merge_requests_for_source_branch.each do |merge_request| + todo_service.merge_request_push(merge_request, @current_user) + end + end + def find_new_commits if branch_added? @commits = [] diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 477c64e7377..026a37997d4 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,8 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + update(merge_request) end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 01586994813..2bb312bb252 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,8 +5,6 @@ module Notes note.author = current_user note.system = false - return unless valid_project?(note) - if note.save # Finish the harder work in the background NewNoteWorker.perform_in(2.seconds, note.id, params) @@ -15,14 +13,5 @@ module Notes note end - - private - - def valid_project?(note) - return false unless project - return true if note.for_commit? - - note.noteable.try(:project) == project - end end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index ba50305dbd5..eb73948006e 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -4,6 +4,10 @@ module Projects @project.issues.visible_to_user(current_user).opened.select([:iid, :title]) end + def milestones + @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title]) + end + def merge_requests @project.merge_requests.opened.select([:iid, :title]) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 501e58c1407..61cac5419ad 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -6,6 +6,7 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) + import_data = params.delete(:import_data) @project = Project.new(params) @@ -49,22 +50,20 @@ module Projects @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) end - Project.transaction do - @project.save + save_project_and_import_data(import_data) - if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository - end - end + @project.import_start if @project.import? after_create_actions if @project.persisted? + if @project.errors.empty? + @project.add_import_job if @project.import? + else + fail(error: @project.errors.full_messages.join(', ')) + end @project rescue => e - message = "Unable to save project: #{e.message}" - Rails.logger.error(message) - @project.errors.add(:base, message) if @project - @project + fail(error: e.message) end protected @@ -93,8 +92,30 @@ module Projects unless @project.group @project.team << [current_user, :master, current_user] end + end - @project.import_start if @project.import? + def save_project_and_import_data(import_data) + Project.transaction do + @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data + + if @project.save && !@project.import? + raise 'Failed to create repository' unless @project.create_repository + end + end + end + + def fail(error:) + message = "Unable to save project. Error: #{error}" + message << "Project ID: #{@project.id}" if @project && @project.id + + Rails.logger.error(message) + + if @project && @project.import? + @project.errors.add(:base, message) + @project.mark_import_as_failed(message) + end + + @project end end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 19aab999e00..f09072975c3 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -26,6 +26,10 @@ module Projects Project.transaction do 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 @@ -35,7 +39,7 @@ module Projects end end - log_info("Project \"#{project.name}\" was removed") + log_info("Project \"#{project.path_with_namespace}\" was removed") system_hook_service.execute_hooks_for(project, :destroy) true end @@ -59,6 +63,12 @@ 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/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 0577ae778d5..de6dc38cc8e 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -3,7 +3,7 @@ module Projects def execute new_params = { forked_from_project_id: @project.id, - visibility_level: @project.visibility_level, + visibility_level: allowed_visibility_level, description: @project.description, name: @project.name, path: @project.path, @@ -19,5 +19,17 @@ module Projects new_project = CreateService.new(current_user, new_params).execute new_project end + + private + + def allowed_visibility_level + project_level = @project.visibility_level + + if Gitlab::VisibilityLevel.non_restricted_level?(project_level) + project_level + else + Gitlab::VisibilityLevel.highest_allowed_level + end + end end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 3b7c36f0908..43db29315a1 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -22,7 +22,7 @@ module Projects end def execute - raise LeaseTaken if !try_obtain_lease + raise LeaseTaken unless try_obtain_lease GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index ef15ef6a473..c4838d31f2f 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -39,7 +39,7 @@ module Projects begin gitlab_shell.import_repository(project.path_with_namespace, project.import_url) rescue Gitlab::Shell::Error => e - raise Error, e.message + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 111b3ec05ea..03b57dea51e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,6 +34,11 @@ 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') + end + project.expire_caches_before_rename(old_path) # Apply new namespace id and visibility level diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index e43b5b51e5b..1fb72cf89e9 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -85,7 +85,7 @@ class SystemHooksService path_with_namespace: model.path_with_namespace, project_id: model.id, owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4bdb1b0c074..4e8fa0818b9 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -169,12 +169,33 @@ class SystemNoteService # # Returns the created Note object def self.change_title(noteable, project, author, old_title) - return unless noteable.respond_to?(:title) + new_title = noteable.title.dup - body = "Title changed from **#{old_title}** to **#{noteable.title}**" + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs + + marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true) + marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) + + body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**" create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when the confidentiality changes + # + # issue - Issue object + # project - Project owning the issue + # author - User performing the change + # + # Example Note text: + # + # "Made the issue confidential" + # + # Returns the created Note object + def self.change_issue_confidentiality(issue, project, author) + body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' + create_note(noteable: issue, project: project, author: author, note: body) + end + # Called when a branch in Noteable is changed # # noteable - Noteable object diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 42c5bca90fd..4bf4e144727 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -80,6 +80,30 @@ class TodoService mark_pending_todos_as_done(merge_request, current_user) end + # When a build fails on the HEAD of a merge request we should: + # + # * create a todo for that user to fix it + # + def merge_request_build_failed(merge_request) + create_build_failed_todo(merge_request) + end + + # When a new commit is pushed to a merge request we should: + # + # * mark all pending todos related to the merge request for that user as done + # + def merge_request_push(merge_request, current_user) + mark_pending_todos_as_done(merge_request, current_user) + end + + # When a build is retried to a merge request we should: + # + # * mark all pending todos related to the merge request for the author as done + # + def merge_request_build_retried(merge_request) + mark_pending_todos_as_done(merge_request, merge_request.author) + end + # When create a note we should: # # * mark all pending todos related to the noteable for the note author as done @@ -145,6 +169,12 @@ class TodoService create_todos(mentioned_users, attributes) end + def create_build_failed_todo(merge_request) + author = merge_request.author + attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED) + create_todos(author, attributes) + end + def attributes_for_target(target) attributes = { project_id: target.project.id, diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb index 9162f128602..4c0a2c6b4d8 100644 --- a/app/services/wiki_pages/base_service.rb +++ b/app/services/wiki_pages/base_service.rb @@ -6,9 +6,8 @@ module WikiPages object_kind: page.class.name.underscore, user: current_user.hook_attrs, project: @project.hook_attrs, - object_attributes: page.hook_attrs, - # DEPRECATED - repository: @project.hook_attrs.slice(:name, :url, :description, :homepage) + wiki: @project.wiki.hook_attrs, + object_attributes: page.hook_attrs } page_url = Gitlab::UrlBuilder.build(page) diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 2ab01704b77..862b86d9d4a 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -16,7 +16,7 @@ .light.small = time_ago_with_tooltip(abuse_report.created_at) %td - = markdown(abuse_report.message.squish!, pipeline: :single_line) + = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) %td - if user = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e0d8d16a954..f149f9eb431 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -103,12 +103,25 @@ = f.label :signup_enabled do = f.check_box :signup_enabled Sign-up enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :send_user_confirmation_email do + = f.check_box :send_user_confirmation_email + Send confirmation email on sign-up .form-group .col-sm-offset-2.col-sm-10 .checkbox = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled + - if omniauth_enabled? && button_based_providers.any? + .form-group + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' + .col-sm-10 + .btn-group{ data: { toggle: 'buttons' } } + - oauth_providers_checkboxes.each do |source| + = source .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 @@ -165,6 +178,14 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' + - if Gitlab.config.registry.enabled + %fieldset + %legend Container Registry + .form-group + = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :container_registry_token_expire_delay, class: 'form-control' + %fieldset %legend Metrics %p diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 804d7851bdb..d74cf8598e8 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -20,7 +20,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'running').capitalize} builds + #{(@scope || 'all').capitalize} builds %ul.content-list - if @builds.blank? @@ -47,4 +47,3 @@ = render "admin/builds/build", build: build = paginate @builds, theme: 'gitlab' - diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml new file mode 100644 index 00000000000..c2313986a7f --- /dev/null +++ b/app/views/admin/health_check/show.html.haml @@ -0,0 +1,49 @@ +- page_title "Health Check" + +%h3.page-title + Health Check +.bs-callout.clearfix + .pull-left + %p + Access token is + %code#health-check-token= current_application_settings.health_check_access_token + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token +%p.light + Health information can be retrieved as plain text, JSON, or XML using: + %ul + %li + %code= health_check_url(token: current_application_settings.health_check_access_token) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) + +%p.light + You can also ask for the status of specific services: + %ul + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) + +%hr +.panel.panel-default + .panel-heading + Current Status: + - if @errors.blank? + = icon('circle', class: 'cgreen') + Healthy + - else + = icon('warning', class: 'cred') + Unhealthy + .panel-body + - if @errors.blank? + No Health Problems Detected + - else + = @errors diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 6745e58deca..36b21eefdee 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -11,18 +11,10 @@ = link_to admin_runner_path(runner) do = runner.short_sha %td - .runner-description - = runner.description - %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) - .runner-description-form.hide - = form_for [:admin, runner], remote: true, html: { class: 'form-inline' } do |f| - .form-group - = f.text_field :description, class: 'form-control' - = f.submit 'Save', class: 'btn' - %span (#{link_to 'cancel', '#', class: 'cancel'}) + = runner.description %td - if runner.shared? - \- + n/a - else = runner.projects.count(:all) %td diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 8700b4820cd..c3784bf7192 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -9,8 +9,6 @@ %span.runner-state.runner-state-specific Specific - - - if @runner.shared? .bs-callout.bs-callout-success %h4 This runner will process builds from ALL UNASSIGNED projects @@ -22,25 +20,9 @@ %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr -= form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup builds to only use runners with specific tags - .form-actions - = f.submit 'Save', class: 'btn btn-save' + +.append-bottom-20 + = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner) .row .col-md-6 diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 0d7b1b30dc3..83c0c6da21b 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -6,8 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id issues_dashboard_url xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end - diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index d4daf07c6c0..fb5be63b472 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id dashboard_projects_url xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(partial: 'events/event', collection: @events) if @events.any? end diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index aa0aff86d4d..98f302d2f93 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,13 +1,15 @@ %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' - .todo-title.title - %span.author-name - - if todo.author - = link_to_author(todo) - - else - (removed) + - unless todo.build_failed? + = todo_target_state_pill(todo) + + %span.author-name + - if todo.author + = link_to_author(todo) + - else + (removed) %span.todo-label = todo_action_name(todo) - if todo.target diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb deleted file mode 100644 index c6fa8f0ee36..00000000000 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -

    Welcome <%= @resource.name %>!

    - -<% if @resource.unconfirmed_email.present? %> -

    You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:

    -<% else %> -

    You can confirm your account through the link below:

    -<% end %> - -

    <%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %>

    diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml new file mode 100644 index 00000000000..086bb8e083d --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -0,0 +1,16 @@ +.center + - if @resource.unconfirmed_email.present? + #content + %h2= @resource.unconfirmed_email + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) + - else + #content + - if Gitlab.com? + %h2 Thanks for signing up to GitLab! + - else + %h2 Welcome, #{@resource.name}! + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb new file mode 100644 index 00000000000..9f76edb76a4 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.text.erb @@ -0,0 +1,9 @@ +Welcome, <%= @resource.name %>! + +<% if @resource.unconfirmed_email.present? %> +You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below: +<% else %> +You can confirm your account through the link below: +<% end %> + +<%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index d65fa60025c..28194506acc 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -4,7 +4,7 @@ = render 'devise/shared/signin_box' -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if omniauth_enabled? && devise_mapping.omniauthable? + - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? .clearfix.prepend-top-20 = render 'devise/shared/omniauth_box' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index c9d1e454a5e..8c6a1552a53 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -4,6 +4,7 @@ %h3 Two-factor Authentication .login-body = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = f.hidden_field :remember_me, value: params[resource_name][:remember_me] = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index ecf680e7b23..de18bc2d844 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,7 +1,7 @@ %p %span.light Sign in with   - - providers = button_based_providers + - providers = enabled_button_based_providers - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index eae80e5210f..ce050007204 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -1,4 +1,4 @@ -%h3.page-title Authorize required +%h3.page-title Authorization required %main{:role => "main"} %p.h4 Authorize diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index dce4081288c..1bc9f604438 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id]) · - = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line + = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder new file mode 100644 index 00000000000..7890e717aa7 --- /dev/null +++ b/app/views/events/_event.atom.builder @@ -0,0 +1,20 @@ +return unless event.visible_to_user?(current_user) + +xml.entry do + xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" + xml.link href: event_feed_url(event) + xml.title truncate(event_feed_title(event), length: 80) + xml.updated event.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) + + xml.author do + xml.name event.author_name + xml.email event.author_email + end + + xml.summary(type: "xhtml") do |summary| + event_summary = event_feed_summary(event) + + summary << event_summary unless event_summary.nil? + end +end diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index fad65310021..083c3936212 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(issue.description, pipeline: :atom, project: issue.project) + = markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index 19bdc7b9ca5..d7e05600627 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(merge_request.description, pipeline: :atom, project: merge_request.project) + = markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index b730ebbd5f9..1154f982821 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(note.note, pipeline: :atom, project: note.project) + = markdown(note.note, pipeline: :atom, project: note.project, author: note.author) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index b271b9daff1..28bee1d0a33 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project) + %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author) - if event.commits_count > 15 %p %i diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index c994e3b997d..c7f29f2fc0e 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -4,7 +4,7 @@ = event_action_name(event) - if event.target - %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target] + %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title = event_preposition(event) diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 235bd46107e..dc4ff17e31a 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -15,7 +15,7 @@ %ul.well-list.event_commits - few_commits = event.commits[0...2] - few_commits.each do |commit| - = render "events/commit", commit: commit, project: project + = render "events/commit", commit: commit, project: project, event: event - create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project) - if event.commits_count > 1 diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index dc76599b776..71cc4d87b1f 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -4,7 +4,7 @@ .nav-block - if current_user .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 486d1d8587a..c19671295af 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,13 +1,10 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@user.name} issues" - xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" - xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" - xml.id issues_dashboard_url + xml.title "#{@group.name} issues" + xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: issues_group_url, rel: "alternate", type: "text/html" + xml.id issues_group_url xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end - diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index c66b82bb484..b68bf444d27 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id group_url(@group) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index e3a356b5379..aedb8468eca 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -47,7 +47,7 @@ %td.import-target = repo["path_with_namespace"] %td.import-actions.job-status - = button_tag class: "btn js-add-to-import" do + = button_tag class: "btn btn-import js-add-to-import" do Import = icon("spinner spin", class: "loading-icon") diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder new file mode 100644 index 00000000000..68a2d19e58d --- /dev/null +++ b/app/views/issues/_issue.atom.builder @@ -0,0 +1,14 @@ +xml.entry do + xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue) + xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) + xml.title truncate(issue.title, length: 80) + xml.updated issue.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) + + xml.author do |author| + xml.name issue.author_name + xml.email issue.author_email + end + + xml.summary issue.title +end diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml index ada7306d98d..e7a70e3bb28 100644 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ b/app/views/kaminari/gitlab/_first_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the first page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li.first diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 3ffd12f8587..80ca30f36e6 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -1,7 +1,7 @@ -# Non-link tag that stands for skipped pages... -# available local variables -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li{class: "page"} diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml index 3431d029bcc..53f780d1d1b 100644 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ b/app/views/kaminari/gitlab/_last_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the last page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li.last diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index c805914fc3f..125f09777ba 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the next page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.last? diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index a52d883b9a8..522e4d1d05f 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -3,7 +3,7 @@ -# page: a page object for "this" page -# url: url to this page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li{class: "page#{' active' if page.current?}"} diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index a12c53bcfe7..f5e0d2ed3f3 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -1,7 +1,7 @@ -# The container tag -# available local variables -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside @@ -9,7 +9,7 @@ %div.gl-pagination %ul.pagination.clearfix - unless current_page.first? - = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages + = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages = prev_page_tag - each_page do |page| - if page.left_outer? || page.right_outer? || page.inside_window? @@ -18,5 +18,5 @@ = gap_tag = next_page_tag - unless current_page.last? - = last_page_tag unless num_pages < 5 + = last_page_tag unless total_pages < 5 diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index afb20455e0a..7edf10498a8 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the previous page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.first? diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 79cdbac1f37..b30fb0a5da9 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,6 +30,9 @@ = javascript_include_tag "application" + - if page_specific_javascripts + = javascript_include_tag page_specific_javascripts, {"data-turbolinks-track" => true} + = csrf_meta_tags = include_gon diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 3c3bc41bf0e..1e961853c70 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,4 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - = render "layouts/broadcast" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -26,7 +25,8 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } + .content-wrapper{ class: "#{layout_nav_class}" } + = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message %div{ class: (container_class unless @no_container) } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 6b208c3d0bb..b49207fc315 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,11 +6,8 @@ .search.search-form{class: "#{'has-location-badge' if label.present?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - .search-location-badge - - if label.present? - %span.location-badge - %i.location-text - = label + - if label.present? + .location-badge= label .search-input-wrap .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml new file mode 100644 index 00000000000..c258eafdd51 --- /dev/null +++ b/app/views/layouts/devise_mailer.html.haml @@ -0,0 +1,34 @@ +!!! 5 +%html + %head + %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type') + = stylesheet_link_tag 'mailers/devise' + + %body + %table#wrapper + %tr + %td + %table#header + %td{valign: "top"} + = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') + + %table#body + %tr + %td#body-container + = yield + + - if Gitlab.com? + %table#footer + %tr + %td#tanuki + = image_tag('mailers/gitlab_tanuki_2x.png', alt: 'GitLab Logo') + %tr + %td#tagline + Everyone can contribute + %tr + %td#social + = link_to 'Blog', 'https://about.gitlab.com/blog/' + = link_to 'Twitter', 'https://twitter.com/gitlab' + = link_to 'Facebook', 'https://www.facebook.com/gitlab/' + = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg' + = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com' diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 86930d4eaaf..c33740e23fa 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,9 +1,12 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } .header-content - %button.navbar-toggle{type: 'button'} + %button.side-nav-toggle{type: 'button'} %span.sr-only Toggle navigation = icon('bars') + %button.navbar-toggle{type: 'button'} + %span.sr-only Toggle navigation + = icon('angle-left') .navbar-collapse.collapse %ul.nav.navbar-nav @@ -24,8 +27,9 @@ %li = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('bell fw') - %span.badge.todos-pending-count - = todos_pending_count + - unless todos_pending_count == 0 + %span.badge.todos-pending-count + = todos_pending_count - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b93729..f292730fe45 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -41,6 +41,11 @@ = icon('file-text fw') %span Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + = icon('medkit fw') + %span + Health Check = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index fad4224e945..306ebd5fcf7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do - = link_to dashboard_projects_path, title: 'Projects' do + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = icon('bookmark fw') %span Projects @@ -11,7 +11,7 @@ Todos %span.count.todos-pending-count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = icon('dashboard fw') %span Activity @@ -26,17 +26,17 @@ %span Milestones = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = icon('exclamation-circle fw') %span Issues - %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_issues_count) = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_merge_request_count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 3438005863a..de15add3617 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,37 +1,40 @@ -= render 'layouts/nav/group_settings' +%div{ class: nav_control_class } + = render 'layouts/nav/group_settings' -%ul.nav-links - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: 'Home' do - = icon('group fw') - %span - Group - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - = icon('dashboard fw') - %span - Activity - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones - = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues' do - = icon('exclamation-circle fw') - %span - Issues - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - = icon('tasks fw') - %span - Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members' do - = icon('users fw') - %span - Members + %ul.nav-links.scrolling-tabs + .fade-left + = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = link_to group_path(@group), title: 'Home' do + = icon('group fw') + %span + Group + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + = icon('dashboard fw') + %span + Activity + = nav_link(controller: [:group, :milestones]) do + = link_to group_milestones_path(@group), title: 'Milestones' do + = icon('clock-o fw') + %span + Milestones + = nav_link(path: 'groups#issues') do + = link_to issues_group_path(@group), title: 'Issues' do + = icon('exclamation-circle fw') + %span + Issues + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(issues.count) + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + = icon('tasks fw') + %span + Merge Requests + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(controller: [:group_members]) do + = link_to group_group_members_path(@group), title: 'Members' do + = icon('users fw') + %span + Members + .fade-right diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index e391ec7f2b7..0b2673f1a82 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,7 +1,7 @@ - if current_user - if access = @group.users.find_by(id: current_user.id) .controls - %span.dropdown.group-settings-dropdown + .dropdown.group-settings-dropdown %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} = icon('cog') = icon('caret-down') diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d730840d63a..2efc6c48a48 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,4 +1,5 @@ -%ul.nav-links +%ul.nav-links.scrolling-tabs + .fade-left = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') @@ -47,3 +48,4 @@ = icon('history fw') %span Audit Log + .fade-right diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 479bde33719..9792c1c93b4 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,131 +1,131 @@ -%ul.nav.nav-sidebar - - if @project.group - = nav_link do - = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') +- if current_user + .controls + - access = user_max_access_in_project(current_user.id, @project) + - can_edit = can?(current_user, :admin_project, @project) + .dropdown.project-settings-dropdown + %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + = render 'layouts/nav/project_settings' + %li.divider + - if can_edit + %li + = link_to edit_project_path(@project) do + Edit Project + - if access + %li + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave Project + +%div{ class: nav_control_class } + %ul.nav-links.scrolling-tabs + .fade-left + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = icon('bookmark fw') %span - Go to group - - else - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') + Project + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + = icon('dashboard fw') %span - Go to dashboard - - %li.separate-item - - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - = icon('bookmark fw') - %span - Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - = icon('dashboard fw') - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') - %span - Files - - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') - %span - Commits + Activity + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do + = icon('code fw') + %span + Code + + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + = icon('ship fw') + %span + Pipelines + + - if project_nav_tab? :container_registry + = nav_link(controller: %w(container_registry)) do + = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + = icon('hdd-o fw') + %span + Container Registry + + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do + = icon('area-chart fw') + %span + Graphs + + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + = icon('clock-o fw') + %span + Milestones + + - if project_nav_tab? :issues + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = icon('exclamation-circle fw') + %span + Issues + - if @project.default_issues_tracker? + %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = icon('tasks fw') + %span + Merge Requests + %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + = icon('tags fw') + %span + Labels + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + = icon('book fw') + %span + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + = icon('clipboard fw') + %span + Snippets + + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network + + -# Shortcut to create a new issue + %li.hidden + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do + Create a new issue - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span + -# Shortcut to builds page + - if project_nav_tab? :builds + %li.hidden + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do Builds - %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - = icon('area-chart fw') - %span - Graphs - - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones - - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') - %span - Issues - - if @project.default_issues_tracker? - %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) - - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') - %span - Merge Requests - %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - - if project_nav_tab? :team - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do - = icon('users fw') - %span - Members - - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - = icon('tags fw') - %span - Labels - - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - = icon('book fw') - %span - Wiki - - if project_nav_tab? :forks - = nav_link(controller: :forks, action: :index) do - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do - = icon('code-fork fw') - %span - Forks - - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do - = icon('clipboard fw') - %span - Snippets - - - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings' do - = icon('cogs fw') - %span - Settings - - -# Global shortcut to network page for compatibility - - if project_nav_tab? :network - %li.hidden - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits - -# Shortcut to create a new issue - %li.hidden - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do - Create a new issue + .fade-right diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index d429a928464..885e78d38c6 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,63 +1,45 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to project_path(@project), title: 'Go to project', class: 'back-link' do - = icon('caret-square-o-left fw') +- if project_nav_tab? :team + = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span - Go to project + Members - %li.separate-item - - %ul.sidebar-subnav - = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project Settings' do - = icon('pencil-square-o fw') - %span - Project Settings - - if @project.allowed_to_share_with_group? - = nav_link(controller: :group_links) do - = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do - = icon('share-square-o fw') - %span - Groups - = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - = icon('key fw') - %span - Deploy Keys - = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do - = icon('link fw') - %span - Webhooks - = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do - = icon('cogs fw') - %span - Services - = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - = icon('lock fw') - %span - Protected Branches +- if @project.allowed_to_share_with_group? + = nav_link(controller: :group_links) do + = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do + %span + Groups += nav_link(controller: :deploy_keys) do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do + %span + Deploy Keys += nav_link(controller: :hooks) do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do + %span + Webhooks += nav_link(controller: :services) do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do + %span + Services += nav_link(controller: :protected_branches) do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do + %span + Protected Branches - - if @project.builds_enabled? - = nav_link(controller: :runners) do - = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do - = icon('cog fw') - %span - Runners - = nav_link(controller: :variables) do - = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do - = icon('code fw') - %span - Variables - = nav_link(controller: :triggers) do - = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do - = icon('retweet fw') - %span - Triggers - = nav_link(controller: :badges) do - = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do - = icon('star-half-empty fw') - %span - Badges +- if @project.builds_enabled? + = nav_link(controller: :runners) do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do + %span + Runners + = nav_link(controller: :variables) do + = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do + %span + Variables + = nav_link(controller: :triggers) do + = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do + %span + Triggers + = nav_link(controller: :badges) do + = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do + %span + Badges diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 2997f59d946..dde2e2889dc 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -4,6 +4,7 @@ %title GitLab = stylesheet_link_tag 'notify' + = yield :head %body %div.content = yield diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 6dfe7fbdae8..20d6cdf7246 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,7 +1,7 @@ - page_title @project.name_with_namespace - page_description @project.description unless page_description - header_title project_title(@project) unless header_title -- sidebar "project" unless sidebar +- nav "project" - content_for :scripts_body_top do - project = @target_project || @project diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index 59ce38f67bb..4bc94bd132d 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,5 +1,4 @@ - page_title "Settings" -- header_title project_title(@project, "Settings", edit_project_path(@project)) -- sidebar "project_settings" +- nav "project" = render template: "layouts/project" diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 12ded41fbf2..e9c66170877 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -2,4 +2,4 @@ %div #{link_to @note.author_name, user_url(@note.author)} wrote: %div - = markdown(@note.note, pipeline: :email) + = markdown(@note.note, pipeline: :email, author: @note.author) diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index ad3ab2525bb..f42b150c0d6 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -2,7 +2,7 @@ %div #{link_to @issue.author_name, user_url(@issue.author)} wrote: -if @issue.description - = markdown(@issue.description, pipeline: :email) + = markdown(@issue.description, pipeline: :email, author: @issue.author) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 23423e7d981..158404de396 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -9,4 +9,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = markdown(@merge_request.description, pipeline: :email) + = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 65f0e4c4068..a3643a00cfe 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,7 @@ -- if @note.diff_file_name +- if @note.legacy_diff_note? %p.details New comment on diff for - = link_to @note.diff_file_name, @target_url + = link_to @note.diff_file_path, @target_url \: = render 'note_message' diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index f2e405b14fd..f1532371b2e 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,3 +1,6 @@ += content_for :head do + = stylesheet_link_tag 'mailers/repository_push_email' + %h3 #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))} @@ -43,26 +46,38 @@ = diff.new_path - unless @message.disable_diffs? - %h4 Changes: - - @message.diffs.each_with_index do |diff, i| - %li{id: "diff-#{i}"} - %a{href: @message.target_url + "#diff-#{i}"} - - if diff.deleted_file - %strong - = diff.old_path - deleted - - elsif diff.renamed_file - %strong - = diff.old_path - → - %strong - = diff.new_path - - else - %strong - = diff.new_path - %hr - = color_email_diff(diff.diff) - %br + - diff_files = @message.diffs - - if @message.compare_timeout - %h5 Huge diff. To prevent performance issues changes are hidden + - if @message.compare_timeout + %h5 The diff was not included because it is too large. + - else + %h4 Changes: + - diff_files.each_with_index do |diff_file, i| + %li{id: "diff-#{i}"} + %a{href: @message.target_url + "#diff-#{i}"}< + - if diff_file.deleted_file + %strong< + = diff_file.old_path + deleted + - elsif diff_file.renamed_file + %strong< + = diff_file.old_path + → + %strong< + = diff_file.new_path + - else + %strong< + = diff_file.new_path + - if diff_file.too_large? + The diff for this file was not included because it is too large. + - else + %hr + - diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last + - blob = @message.project.repository.blob_for_diff(diff_commit, diff_file) + - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) + %table.code.white + - diff_file.highlighted_diff_lines.each do |line| + = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true} + - else + No preview for this file type + %br diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 53869e36b28..5ac23aa3997 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -25,24 +25,28 @@ - else \- #{diff.new_path} - unless @message.disable_diffs? - \ - \ - Changes: - - @message.diffs.each do |diff| + - if @message.compare_timeout \ - \===================================== - - if diff.deleted_file - #{diff.old_path} deleted - - elsif diff.renamed_file - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path - \===================================== - != diff.diff - - if @message.compare_timeout - \ - \ - Huge diff. To prevent performance issues it was hidden + \ + The diff was not included because it is too large. + - else + \ + \ + Changes: + - @message.diffs.each do |diff_file| + \ + \===================================== + - if diff_file.deleted_file + #{diff_file.old_path} deleted + - elsif diff_file.renamed_file + #{diff_file.old_path} → #{diff_file.new_path} + - else + = diff_file.new_path + \===================================== + - if diff_file.too_large? + The diff for this file was not included because it is too large. + - else + != diff_file.diff.diff - if @message.target_url \ \ diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index afd3d79321f..01ac8161945 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -70,7 +70,7 @@ - if current_user.can_change_username? .row.prepend-top-default .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0.change-username-title + %h4.prepend-top-0.warning-title Change username %p Changing your username will change path to all personal projects! @@ -94,7 +94,7 @@ - if signup_enabled? .row.prepend-top-default .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0.remove-account-title + %h4.prepend-top-0.danger-title Remove account .col-lg-9 - if @user.can_be_removed? diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index bfe53be6854..1b1b16d656f 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -5,7 +5,7 @@ %h4.prepend-top-0 Application theme %p - This setting allows you to customize the appearance of the site, ex. sidebar. + This setting allows you to customize the appearance of the site, e.g. the sidebar. .col-lg-9.application-theme - Gitlab::Themes.each do |theme| = label_tag do diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 0de019983ca..0568c2d305e 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -1,74 +1,65 @@ %fieldset.builds-feature - %legend - Builds: - + %h5.prepend-top-0 + Builds - unless @repository.gitlab_ci_yml .form-group - .col-sm-offset-2.col-sm-10 - %p Builds need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' - %hr - + %p Builds need to be configured before you can begin using Continuous Integration. + = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' .form-group - .col-sm-offset-2.col-sm-10 - %p Get recent application code using the following command: - .radio - = f.label :build_allow_git_fetch_false do - = f.radio_button :build_allow_git_fetch, 'false' - %strong git clone - %br - %span.descr Slower but makes sure you have a clean dir before every build - .radio - = f.label :build_allow_git_fetch_true do - = f.radio_button :build_allow_git_fetch, 'true' - %strong git fetch - %br - %span.descr Faster + %p Get recent application code using the following command: + .radio + = f.label :build_allow_git_fetch_false do + = f.radio_button :build_allow_git_fetch, 'false' + %strong git clone + %br + %span.descr Slower but makes sure you have a clean dir before every build + .radio + = f.label :build_allow_git_fetch_true do + = f.radio_button :build_allow_git_fetch, 'true' + %strong git fetch + %br + %span.descr Faster .form-group - = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' - .col-sm-10 - = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' - %p.help-block per build in minutes + = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light' + = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' + %p.help-block per build in minutes .form-group - = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label' - .col-sm-10 - .input-group - %span.input-group-addon / - = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' - %span.input-group-addon / - %p.help-block - We will use this regular expression to find test coverage output in build trace. - Leave blank if you want to disable this feature - .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: - %ul - %li - Simplecov (Ruby) - - %code \(\d+.\d+\%\) covered - %li - pytest-cov (Python) - - %code \d+\%\s*$ - %li - phpunit --coverage-text --colors=never (PHP) - - %code ^\s*Lines:\s*\d+.\d+\% - %li - gcovr (C/C++) - - %code ^TOTAL.*\s+(\d+\%)$ - %li - tap --coverage-report=text-summary (Node.js) - - %code ^Statements\s*:\s*([^%]+) + = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' + .input-group + %span.input-group-addon / + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + %p.help-block + We will use this regular expression to find test coverage output in build trace. + Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%\s*$ + %li + phpunit --coverage-text --colors=never (PHP) - + %code ^\s*Lines:\s*\d+.\d+\% + %li + gcovr (C/C++) - + %code ^TOTAL.*\s+(\d+\%)$ + %li + tap --coverage-report=text-summary (Node.js) - + %code ^Statements\s*:\s*([^%]+) .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :public_builds do - = f.check_box :public_builds - %strong Public builds - .help-block Allow everyone to access builds for Public and Internal projects + .checkbox + = f.label :public_builds do + = f.check_box :public_builds + %strong Public builds + .help-block Allow everyone to access builds for Public and Internal projects - .form-group - = f.label :runners_token, "Runners token", class: 'control-label' - .col-sm-10 - = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' - %p.help-block The secure token used to checkout project. + .form-group.append-bottom-0 + = f.label :runners_token, "Runners token", class: 'label-light' + = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' + %p.help-block The secure token used to checkout project. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 9b5de17dd3b..57c3d1b0a65 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,59 +1,37 @@ - empty_repo = @project.empty_repo? .project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)} - .project-identicon-holder - = project_icon(@project, alt: '', class: 'project-avatar avatar s90') - .cover-title.project-home-desc - %h1 - = @project.name - %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} - = visibility_level_icon(@project.visibility_level, fw: false) - - - if @project.description.present? - .cover-desc.project-home-desc - = markdown(@project.description, pipeline: :description) - - - if forked_from_project = @project.forked_from_project - .cover-desc - Forked from - = link_to project_path(forked_from_project) do - = forked_from_project.namespace.try(:name) - - .cover-controls - - if current_user - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do - = icon('rss') - - access = user_max_access_in_project(current_user.id, @project) - - can_edit = can?(current_user, :admin_project, @project) - - if access || can_edit - %span.dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} - = icon('cog') - = icon('angle-down') - %ul.dropdown-menu.dropdown-menu-right - - if can_edit - %li - = link_to edit_project_path(@project) do - Edit Project - - if access - %li - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), - data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do - Leave Project - - .project-repo-buttons - .split-one.count-buttons - = render 'projects/buttons/star' - = render 'projects/buttons/fork' - - .clone-row - .project-clone-holder - = render "shared/clone_panel" - - .split-repo-buttons - .btn-group.pull-left - = render "projects/buttons/download" - = render 'projects/buttons/dropdown' - + .container-fluid.container-limited + .row + .project-image-container + = project_icon(@project, alt: '', class: 'project-avatar avatar s70') + .project-info + .cover-title.project-home-desc + %h1 + = @project.name + %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} + = visibility_level_icon(@project.visibility_level, fw: false) + + - if @project.description.present? + .cover-desc.project-home-desc + = markdown(@project.description, pipeline: :description) + + - if forked_from_project = @project.forked_from_project + .cover-desc + Forked from + = link_to project_path(forked_from_project) do + = forked_from_project.namespace.try(:name) + + .project-repo-buttons + .count-buttons + = render 'projects/buttons/star' + = render 'projects/buttons/fork' + + .project-clone-holder + = render "shared/clone_panel" + + .project-repo-buttons.btn-group.project-right-buttons + = render "projects/buttons/download" + = render 'projects/buttons/dropdown' = render 'projects/buttons/notifications' :javascript diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8de44a6c914..81afea2c60a 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -8,7 +8,7 @@ %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview %li.pull-right - %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } + %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen .md-write-holder diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e1e35013968..413477a2d3a 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -4,5 +4,5 @@ = f.text_area attr, class: classes, placeholder: placeholder - else = text_area_tag attr, nil, class: classes, placeholder: placeholder - %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 69fa4ad37c4..3c0f01cbf6f 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,5 +1,4 @@ - page_title "Activity" -- header_title project_title(@project, "Activity", activity_project_path(@project)) = render 'projects/last_push' diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 49f95ff37db..ede01dcc1aa 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,5 +1,4 @@ - page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' -= render 'projects/builds/header_title' .top-block.row-content-block.clearfix .pull-right diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml index c22384ddf46..ee63bc55a30 100644 --- a/app/views/projects/badges/index.html.haml +++ b/app/views/projects/badges/index.html.haml @@ -1,6 +1,5 @@ - page_title 'Badges' - badges_path = namespace_project_badges_path(@project.namespace, @project) -- header_title project_title(@project, 'Badges', badges_path) .prepend-top-10 .panel.panel-default diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 5f9a92ff93f..377665b096f 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,5 +1,4 @@ - page_title "Blame", @blob.path, @ref -- header_title project_title(@project, "Files", project_files_path(@project)) %h3.page-title Blame view diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index fefa652a3da..4071b59c003 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -16,6 +16,9 @@ .license-selector.js-license-selector.hide = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} + .gitignore-selector.hidden + = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } ) + .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/app/views/projects/blob/_header_title.html.haml b/app/views/projects/blob/_header_title.html.haml deleted file mode 100644 index 78c5ef20a5f..00000000000 --- a/app/views/projects/blob/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Files", project_files_path(@project)) diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index effcce5a1c4..e4f04ca7764 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @blob.path, @ref -= render "header_title" .file-editor %ul.nav-links.no-bottom.js-edit-mode diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 0459699432e..c952bc7e5db 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,5 +1,4 @@ - page_title "New File", @path.presence, @ref -= render "header_title" %h3.page-title New File diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 6988039b6c7..ed670dae88d 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,5 +1,4 @@ - page_title @blob.path, @ref -= render "header_title" = render 'projects/last_push' diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml deleted file mode 100644 index a21ddaf4930..00000000000 --- a/app/views/projects/branches/destroy.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ac7790421a4..08148b1a18b 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,5 +1,4 @@ - page_title "Branches" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block .pull-right diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index c659af6338c..5a6c8c243fa 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Branch" -= render "projects/commits/header_title" - if @error .alert.alert-danger diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml deleted file mode 100644 index 082dab1f5b0..00000000000 --- a/app/views/projects/builds/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Builds", project_builds_path(@project)) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 2e8015d119b..818d5d28f04 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,5 +1,5 @@ - page_title "Builds" -= render "header_title" += render "projects/pipelines/head" .top-area %ul.nav-links @@ -35,9 +35,6 @@ = icon('wrench') %span CI Lint -.row-content-block - #{(@scope || 'running').capitalize} builds from this project - %ul.content-list - if @builds.blank? %li diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c0f7a7686f0..16017c994ba 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,5 +1,5 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" -= render "header_title" +- trace_with_state = @build.trace_with_state .build-page .row-content-block.top-block @@ -85,7 +85,9 @@ %pre.trace#build-trace %code.bash = preserve do - = raw @build.trace_html + = raw trace_with_state[:html] + - if @build.active? + %i{:class => "fa fa-refresh fa-spin"} %div#down-build-trace @@ -216,4 +218,4 @@ :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 1e4c46fca2f..16b8e1cca91 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -2,7 +2,7 @@ .btn-group %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') - %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown + %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - can_create_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - can_create_snippet = can?(current_user, :create_snippet, @project) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 5fb5fe5af2f..34ad9fe2c43 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -12,7 +12,8 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = icon('code-fork fw') Fork - = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do + %div.count-with-arrow %span.arrow %span.count - = @project.forks_count + = link_to namespace_project_forks_path(@project.namespace, @project) do + = @project.forks_count diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index c1e3e5b73a2..1d05da50581 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,11 +1,11 @@ - if @notification_setting = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level - %span.dropdown + .dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') = notification_title(@notification_setting.level) - = icon('angle-down') - %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - NotificationSetting.levels.each do |level| = notification_list_item(level.first, @notification_setting) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 8e95f040273..5bd6e3f0ebc 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,7 +13,9 @@ %strong ##{build.id} - if build.stuck? - %i.fa.fa-warning.text-warning + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - if defined?(commit_sha) && commit_sha %td @@ -70,11 +72,11 @@ .pull-right - if can?(current_user, :read_build, build) && build.artifacts? = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do - %i.fa.fa-download + = icon('download') - if can?(current_user, :update_build, build) - if build.active? = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do - %i.fa.fa-remove.cred + = icon('remove', class: 'cred') - elsif defined?(allow_retry) && allow_retry && build.retryable? = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - %i.fa.fa-refresh + = icon('refresh') diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml new file mode 100644 index 00000000000..5e3a4123a8e --- /dev/null +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -0,0 +1,71 @@ +- status = commit.status +%tr.commit + %td.commit-link + = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do + = ci_icon_for_status(status) + %strong ##{commit.id} + + %td + %div.branch-commit + - if commit.ref + = link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace" + · + = link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace" +   + - if commit.tag? + %span.label.label-primary tag + - elsif commit.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + - if commit.triggered? + %span.label.label-primary triggered + - if commit.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid + - if commit.builds.any?(&:stuck?) + %span.label.label-warning stuck + + %p.commit-title + - if commit_data = commit.commit_data + = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch + + + - stages_status = commit.statuses.stages_status + - stages.each do |stage| + %td + - status = stages_status[stage] + - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - if status + = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = ci_icon_for_status(status) + - else + .light.has-tooltip{ title: tooltip } + \- + + %td + - if commit.started_at && commit.finished_at + %p.duration + #{duration_in_words(commit.finished_at, commit.started_at)} + + %td + .controls.hidden-xs.pull-right + - artifacts = commit.builds.latest.select { |b| b.artifacts? } + - if artifacts.present? + .dropdown.inline.build-artifacts + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + = icon('download') + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - artifacts.each do |build| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do + = icon("download") + %span #{build.name} + + - if can?(current_user, :update_pipeline, @project) + - if commit.retryable? + = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = icon("repeat") + - if commit.cancelable? + = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = icon("remove") diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 5c9a319edeb..7f7a15aa214 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,2 +1,2 @@ - @ci_commits.each do |ci_commit| - = render "ci_commit", ci_commit: ci_commit + = render "ci_commit", ci_commit: ci_commit, pipeline_details: true diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index e849aefb188..32ff4d30977 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -1,24 +1,27 @@ .row-content-block.build-content.middle-block .pull-right - - if can?(current_user, :update_build, @project) + - if can?(current_user, :update_pipeline, ci_commit.project) - if ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + = link_to "Retry failed", retry_namespace_project_pipeline_path(ci_commit.project.namespace, ci_commit.project, ci_commit.id), class: 'btn btn-grouped btn-primary', method: :post - if ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + = link_to "Cancel running", cancel_namespace_project_pipeline_path(ci_commit.project.namespace, ci_commit.project, ci_commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - .oneline - = pluralize ci_commit.statuses.count(:id), "build" - - if ci_commit.ref - for - %span.label.label-info - = ci_commit.ref - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" - - if ci_commit.duration - in - = time_interval_in_words ci_commit.duration + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{ci_commit.id}", namespace_project_pipeline_path(ci_commit.project.namespace, ci_commit.project, ci_commit.id), class: "monospace" + with + = pluralize ci_commit.statuses.count(:id), "build" + - if ci_commit.ref + for + = link_to ci_commit.ref, namespace_project_commits_path(ci_commit.project.namespace, ci_commit.project, ci_commit.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to ci_commit.short_sha, namespace_project_commit_path(ci_commit.project.namespace, ci_commit.project, ci_commit.sha), class: "monospace" + - if ci_commit.duration + in + = time_interval_in_words ci_commit.duration - if ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger @@ -28,7 +31,7 @@ %li= error You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} -- if @project.builds_enabled? && !ci_commit.ci_yaml_file +- if ci_commit.project.builds_enabled? && !ci_commit.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit @@ -38,34 +41,12 @@ %tr %th Status %th Build ID - %th Stage %th Name %th Tags %th Duration %th Finished at - - if @project.build_coverage_enabled? + - if ci_commit.project.build_coverage_enabled? %th Coverage %th - - builds = ci_commit.statuses.latest.ordered - = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true - -- if ci_commit.retried.any? - .row-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Tags - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th - = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false + - ci_commit.statuses.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage) diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml new file mode 100644 index 00000000000..ae7bb01223e --- /dev/null +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -0,0 +1,15 @@ +%tr + %th{colspan: 10} + %strong + %a{name: stage} + - status = statuses.latest.status + %span{class: "ci-status-link ci-status-icon-#{status}"} + = ci_icon_for_status(status) + - if stage +   + = stage.titleize.pluralize + = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true + = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true + %tr + %td{colspan: 10} +   diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 01163e526b2..6674d58417b 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,37 +1,35 @@ -.pull-right.commit-action-buttons - %div - - if @notes_count > 0 - %span.btn.disabled.btn-grouped - %i.fa.fa-comment +.commit-info-row.commit-info-row-header + %span.hidden-xs Authored by + %strong + = commit_author_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.authored_date)} + + .pull-right.commit-action-buttons + - if defined?(@notes_count) && @notes_count > 0 + %span.btn.disabled.btn-grouped.hidden-xs + = icon('comment') = @notes_count - .pull-left.btn-group - %a.btn.btn-grouped.dropdown-toggle{ data: {toggle: :dropdown} } - %i.fa.fa-download - Download as - %span.caret - %ul.dropdown-menu + = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped hidden-xs hidden-sm" do + Browse Files + .dropdown.inline + %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } + %span.hidden-xs Options + %span.caret.commit-options-dropdown-caret + %ul.dropdown-menu.dropdown-menu-align-right + %li.visible-xs-block.visible-sm-block + = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do + Browse Files + - unless @commit.has_been_reverted?(current_user) + %li.clearfix + = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) + %li.clearfix + = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) + %li.divider + %li.dropdown-header + Download - unless @commit.parents.length > 1 %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) - = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do - = icon('files-o') - Browse Files - - unless @commit.has_been_reverted?(current_user) - = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) - = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) - %div - -%p -.commit-info-row - - if @commit.status - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do - = ci_icon_for_status(@commit.status) - build: - = ci_label_for_status(@commit.status) - %span.light Authored by - %strong - = commit_author_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.authored_date)} - if @commit.different_committer? .commit-info-row @@ -41,8 +39,9 @@ #{time_ago_with_tooltip(@commit.committed_date)} .commit-info-row - %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + %span.hidden-xs.hidden-sm Commit + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm" + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace visible-xs-inline visible-sm-inline" = clipboard_button(clipboard_text: @commit.id) %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| @@ -51,12 +50,23 @@ %span.commit-info.branches %i.fa.fa-spinner.fa-spin +- if @commit.status + .commit-info-row + Builds for + = pluralize(@commit.ci_commits.count, 'pipeline') + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do + = ci_icon_for_status(@commit.status) + = ci_label_for_status(@commit.status) + - if @commit.ci_commits.duration + in + = time_interval_in_words @commit.ci_commits.duration + .commit-box.content-block %h3.commit-title - = markdown escape_once(@commit.title), pipeline: :single_line + = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author - if @commit.description.present? %pre.commit-description - = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author)) :javascript $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 7118a4846c6..2f051fb90e0 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,7 +1,7 @@ - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/header_title" + .prepend-top-default = render "commit_box" -= render "ci_menu" += render "ci_menu" = render "builds" diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index e5e3d696035..401cb4f7e30 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,8 +1,6 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description -= render "projects/commits/header_title" - .prepend-top-default = render "commit_box" - if @commit.status diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder new file mode 100644 index 00000000000..1657fb46163 --- /dev/null +++ b/app/views/projects/commits/_commit.atom.builder @@ -0,0 +1,14 @@ +xml.entry do + xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.title truncate(commit.title, length: 80) + xml.updated commit.committed_date.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) + + xml.author do |author| + xml.name commit.author_name + xml.email commit.author_email + end + + xml.summary markdown(commit.description, pipeline: :single_line) +end diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index c7d8c9a0d15..367027182b6 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -17,14 +17,14 @@ .pull-right - if commit.status - = render_ci_status(commit) + = render_commit_status(commit) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - if commit.description? .commit-row-description.js-toggle-content %pre - = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) + = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) .commit-row-info by diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 64e8da9201d..7283a78a64e 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,7 +3,7 @@ - commits, hidden = limited_commits(@commits) -- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| +- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index d1bd76ab529..1c136133ab0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,9 +1,11 @@ %ul.nav-links + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files + = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - %span.badge - = number_with_delimiter(@repository.commit_count) = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do @@ -16,9 +18,7 @@ = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches - %span.badge.js-totalbranch-count= @repository.branch_count = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - %span.badge.js-totaltags-count= @repository.tag_count diff --git a/app/views/projects/commits/_header_title.html.haml b/app/views/projects/commits/_header_title.html.haml deleted file mode 100644 index e4385893dd9..00000000000 --- a/app/views/projects/commits/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Commits", project_commits_path(@project)) diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index e310fafd82c..30bb7412073 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -6,18 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.xmlschema if @commits.any? - @commits.each do |commit| - xml.entry do - xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) - xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) - xml.title truncate(commit.title, length: 80) - xml.updated commit.committed_date.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) - xml.author do |author| - xml.name commit.author_name - xml.email commit.author_email - end - xml.summary markdown(commit.description, pipeline: :single_line) - end - end + xml << render(@commits) if @commits.any? end diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 088eaa28013..2c21923ed4f 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,5 +1,4 @@ - page_title "Commits", @ref -= render "header_title" = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 5e188dd0f3c..0b8ed23b305 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,5 +1,4 @@ - page_title "Compare" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 62525168239..cdc34f51d6d 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,5 +1,4 @@ - page_title "#{params[:from]}...#{params[:to]}" -= render "projects/commits/header_title" = render "projects/commits/head" diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml new file mode 100644 index 00000000000..4e9f936539b --- /dev/null +++ b/app/views/projects/container_registry/_tag.html.haml @@ -0,0 +1,21 @@ +%tr.tag + %td + = escape_once(tag.name) + = clipboard_button(clipboard_text: "docker pull #{tag.path}") + %td + - if layer = tag.layers.first + %span.has-tooltip{ title: "#{layer.revision}" } + = layer.short_revision + - else + \- + %td + = number_to_human_size(tag.total_size) + · + = pluralize(tag.layers.size, "layer") + %td + = time_ago_in_words(tag.created_at) + - 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 + = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml new file mode 100644 index 00000000000..993da27310f --- /dev/null +++ b/app/views/projects/container_registry/index.html.haml @@ -0,0 +1,39 @@ +- page_title "Container Registry" + +%hr + +%ul.content-list + %li.light.prepend-top-default + %p + A 'container image' is a snapshot of a container. + You can host your container images with GitLab. + %br + To start using container images hosted on GitLab you first need to login: + %pre + %code + docker login #{Gitlab.config.registry.host_port} + %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)} . + %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. + + - 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 diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index e230834e8ba..04fbb37d93f 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -19,7 +19,7 @@ %ul.well-list = render @enabled_keys - else - .profile-settings-message.text-center + .settings-message.text-center No deploy keys found. Create one with the form above or add existing one below. %h5.prepend-top-default Deploy keys from projects you have access to (#{@available_project_keys.size}) @@ -27,7 +27,7 @@ %ul.well-list = render @available_project_keys - else - .profile-settings-message.text-center + .settings-message.text-center No deploy keys from your projects could be found. Create one with the form above or add existing one below. - if @available_public_keys.any? %h5.prepend-top-default diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0f04fc5d33c..e5983c58039 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -11,11 +11,9 @@ = link_to "#diff-#{i}" do - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - .filename.old - = old_path + = old_path → - .filename.new - = new_path + = new_path - else %span = diff_file.new_path @@ -41,7 +39,7 @@ .diff-content.diff-wrap-lines - # Skip all non non-supported blobs - - return unless blob.respond_to?('text?') + - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 107097ad963..f1577e8a47b 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -15,7 +15,7 @@ = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " ".html_safe : line.new_pos diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 81948513e43..4ecc9528bd2 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -16,7 +16,7 @@ - else %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) @@ -29,14 +29,14 @@ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - - if @comments_allowed && can?(current_user, :create_note, @project) - = link_to_new_diff_note(right[:line_code], 'new') + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) + = link_to_new_diff_note(new_line_code, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - - if @reply_allowed - - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - - if comments_left.present? || comments_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right + - unless @diff_notes_disabled + - notes_left, notes_right = organize_comments(left, right) + - if notes_left.present? || notes_right.present? + = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right - if diff_file.diff.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e7169d7b599..068593a7dd1 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -6,16 +6,15 @@ %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - last_line = 0 - - raw_diff_lines = diff_file.diff_lines.to_a - diff_file.highlighted_diff_lines.each_with_index do |line, index| - line_code = generate_line_code(diff_file.file_path, line) - last_line = line.new_pos = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} - - if @reply_allowed - - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - - unless comments.empty? - = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text + - unless @diff_notes_disabled + - diff_notes = @grouped_diff_notes[line_code] + - if diff_notes + = render "projects/notes/diff_notes_with_reply", notes: diff_notes - if last_line > 0 = render "projects/diffs/match_line", { line: "", diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 76a4f41193c..18b125ff9d4 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,251 +1,221 @@ -.project-edit-container.prepend-top-default - .project-edit-errors - .project-edit-content - .panel.panel-default - .panel-heading +.project-edit-container + .row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 Project settings - .panel-body - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f| - - %fieldset - .form-group.project_name_holder - = f.label :name, class: 'control-label' do - Project name - .col-sm-10 - = f.text_field :name, class: "form-control", id: "project_name_edit" - - - .form-group - = f.label :description, class: 'control-label' do - Project description - %span.light (optional) - .col-sm-10 - = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - - - unless @project.empty_repo? - .form-group - = f.label :default_branch, "Default Branch", class: 'control-label' - .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) - - - = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project - + .col-lg-9 + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| + %fieldset.append-bottom-0 .form-group - = f.label :tag_list, "Tags", class: 'control-label' - .col-sm-10 - = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" - %p.help-block Separate tags with commas. - - %fieldset.features - %legend - Features: - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :issues_enabled do - = f.check_box :issues_enabled - %strong Issues - %br - %span.descr Lightweight issue tracking system for this project - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :merge_requests_enabled do - = f.check_box :merge_requests_enabled - %strong Merge Requests - %br - %span.descr Submit changes to be merged upstream - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :builds_enabled do - = f.check_box :builds_enabled - %strong Builds - %br - %span.descr Test and deploy your changes before merge - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :wiki_enabled do - = f.check_box :wiki_enabled - %strong Wiki - %br - %span.descr Pages for project documentation - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :snippets_enabled do - = f.check_box :snippets_enabled - %strong Snippets - %br - %span.descr Share code pastes with others out of git repository - - = render 'builds_settings', f: f + = f.label :name, class: 'label-light' do + Project name + = f.text_field :name, class: "form-control", id: "project_name_edit" + .form-group + = f.label :description, class: 'label-light' do + Project description + %span.light (optional) + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - %fieldset.features - %legend - Project avatar: + - unless @project.empty_repo? .form-group - .col-sm-offset-2.col-sm-10 - - if @project.avatar? - = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') - %p.light - - if @project.avatar_in_git - Project avatar in repository: #{ @project.avatar_in_git } - %p.light - - if @project.avatar? - You can change your project avatar here - - else - You can upload a project avatar here - %a.choose-btn.btn.btn-sm.js-choose-project-avatar-button - %i.icon-paper-clip - %span Choose File ... -   - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .light The maximum file size allowed is 200KB. - - if @project.avatar? - %hr - = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - - - .form-actions - = f.submit 'Save changes', class: "btn btn-save" - - - - .danger-settings - .panel.panel-default - .panel-heading Housekeeping - .errors-holder - .panel-body - %p - Runs a number of housekeeping tasks within the current repository, - such as compressing file revisions and removing unreachable objects. - %br - - .form-actions - = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), - method: :post, class: "btn btn-default" - - - if can? current_user, :archive_project, @project - - if @project.archived? - .panel.panel-success - .panel-heading - Unarchive project - .panel-body - %p - Unarchiving the project will mark its repository as active. + = f.label :default_branch, "Default Branch", class: 'label-light' + = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + .form-group.project-visibility-level-holder + = f.label :visibility_level, class: 'label-light' do + Visibility Level + = link_to "(?)", help_page_path("public_access", "public_access") + - if can_change_visibility_level?(@project, current_user) + = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) + - else + .info + = visibility_level_icon(@project.visibility_level) + %strong + = visibility_level_label(@project.visibility_level) + .light= visibility_level_description(@project.visibility_level, @project) + .form-group + = f.label :tag_list, "Tags", class: 'label-light' + = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" + %p.help-block Separate tags with commas. + %hr + %fieldset.features.append-bottom-0 + %h5.prepend-top-0 + Features + .form-group + .checkbox + = f.label :issues_enabled do + = f.check_box :issues_enabled + %strong Issues %br - The project can be committed to. + %span.descr Lightweight issue tracking system for this project + .form-group + .checkbox + = f.label :merge_requests_enabled do + = f.check_box :merge_requests_enabled + %strong Merge Requests %br - %strong Once active this project shows up in the search and on the dashboard. - - .form-actions - = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-success" - - else - .panel.panel-warning - .panel-heading - Archive project - .panel-body - %p - Archiving the project will mark its repository as read-only. + %span.descr Submit changes to be merged upstream + .form-group + .checkbox + = f.label :builds_enabled do + = f.check_box :builds_enabled + %strong Builds + %br + %span.descr Test and deploy your changes before merge + .form-group + .checkbox + = f.label :wiki_enabled do + = f.check_box :wiki_enabled + %strong Wiki %br - It is hidden from the dashboard and doesn't show up in searches. + %span.descr Pages for project documentation + .form-group + .checkbox + = f.label :snippets_enabled do + = f.check_box :snippets_enabled + %strong Snippets %br - %strong Archived projects cannot be committed to! - - .form-actions - = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" - - else - .nothing-here-block Only the project owner can archive a project - - .panel.panel-default.panel.panel-warning - .panel-heading Rename repository - .errors-holder - .panel-body - = form_for([@project.namespace.becomes(Namespace), @project], html: { class: 'form-horizontal' }) do |f| - .form-group.project_name_holder - = f.label :name, class: 'control-label' do - Project name - .col-sm-9 - .form-group - = f.text_field :name, class: "form-control" + %span.descr Share code pastes with others out of git repository + - if Gitlab.config.registry.enabled .form-group - = f.label :path, class: 'control-label' do - %span Path - .col-sm-9 - .form-group - .input-group - .input-group-addon - #{URI.join(root_url, @project.namespace.path)}/ - = f.text_field :path, class: 'form-control' - %ul - %li Be careful. Renaming a project's repository can have unintended side effects. - %li You will need to update your local repositories to point to the new location. - .form-actions - = f.submit 'Rename project', class: "btn btn-warning" - - - if can?(current_user, :change_namespace, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Transfer project - .errors-holder - .panel-body - = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| - .form-group - = label_tag :new_namespace_id, nil, class: 'control-label' do - %span Namespace - .col-sm-9 - .form-group - = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } - %ul - %li Be careful. Changing the project's namespace can have unintended side effects. - %li You can only transfer the project to namespaces you manage. - %li You will need to update your local repositories to point to the new location. - %li Project visibility level will be changed to match namespace rules when transfering to a group. - .form-actions - = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - - else - .nothing-here-block Only the project owner can transfer a project - - - if @project.forked? - - if can?(current_user, :remove_fork_project, @project) - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| - .panel.panel-default.panel.panel-danger - .panel-heading Remove fork relationship - .panel-body - %p - This will remove the fork relationship to source project - #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}. + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry %br - %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - .form-actions - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + %span.descr Enable Container Registry for this repository + %hr + = render 'builds_settings', f: f + %hr + %fieldset.features.append-bottom-default + %h5.prepend-top-0 + Project avatar + .form-group + - if @project.avatar? + = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') + %p.light + - if @project.avatar_in_git + Project avatar in repository: #{ @project.avatar_in_git } + %a.choose-btn.btn.js-choose-project-avatar-button + Browse file... + %span.file_name.prepend-left-default.js-avatar-filename No file chosen + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .help-block The maximum file size allowed is 200KB. + - if @project.avatar? + %hr + = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + = f.submit 'Save changes', class: "btn btn-save" + .row.prepend-top-default + %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Housekeeping + %p.append-bottom-0 + %p + Runs a number of housekeeping tasks within the current repository, + such as compressing file revisions and removing unreachable objects. + .col-lg-9 + = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-save" + %hr + - if can? current_user, :archive_project, @project + .row.prepend-top-default + .col-lg-3 + %h4.warning-title.prepend-top-0 + - if @project.archived? + Unarchive project + - else + Archive project + %p.append-bottom-0 + - if @project.archived? + Unarchiving the project will mark its repository as active. The project can be committed to. + - else + Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. + .col-lg-9 + - if @project.archived? + %p + %strong Once active this project shows up in the search and on the dashboard. + = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" - else - .nothing-here-block Only the project owner can remove the fork relationship. - - - if can?(current_user, :remove_project, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Remove project - .panel-body - = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do - %p - Removing the project will delete its repository and all related resources including issues, merge requests etc. - %br - %strong Removed projects cannot be restored! - .form-actions - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } - - else - .nothing-here-block Only the project owner can remove a project. - + %p + %strong Archived projects cannot be committed to! + = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" + %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0.warning-title + Rename repository + .col-lg-9 + = form_for([@project.namespace.becomes(Namespace), @project]) do |f| + .form-group.project_name_holder + = f.label :name, class: 'label-light' do + Project name + .form-group + = f.text_field :name, class: "form-control" + .form-group + = f.label :path, class: 'label-light' do + %span Path + .form-group + .input-group + .input-group-addon + #{URI.join(root_url, @project.namespace.path)}/ + = f.text_field :path, class: 'form-control' + %ul + %li Be careful. Renaming a project's repository can have unintended side effects. + %li You will need to update your local repositories to point to the new location. + = f.submit 'Rename project', class: "btn btn-warning" + - if can?(current_user, :change_namespace, @project) + %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0.danger-title + Transfer project + .col-lg-9 + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true) do |f| + .form-group + = label_tag :new_namespace_id, nil, class: 'label-light' do + %span Namespace + .form-group + = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } + %ul + %li Be careful. Changing the project's namespace can have unintended side effects. + %li You can only transfer the project to namespaces you manage. + %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + %hr + .row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0.danger-title + Remove fork relationship + %p.append-bottom-0 + %p + This will remove the fork relationship to source project + = succeed "." do + = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + .col-lg-9 + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| + %p + %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + - if can?(current_user, :remove_project, @project) + %hr + .row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0.danger-title + Remove project + %p.append-bottom-0 + Removing the project will delete its repository and all related resources including issues, merge requests etc. + .col-lg-9 + = form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do + %p + %strong Removed projects cannot be restored! + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } .save-project-loader.hide .center @@ -254,5 +224,4 @@ Saving project. %p Please wait a moment, this page will automatically refresh when ready. - = render 'shared/confirm_modal', phrase: @project.path diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 1a2e59752fe..636beb73ec2 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -15,10 +15,14 @@ If you already have files you can push them using command line instructions below. %p Otherwise you can start with adding a - = link_to "README", new_readme_path, class: 'underlined-link' + = succeed ',' do + = link_to "README", new_readme_path, class: 'underlined-link' + a + = succeed ',' do + = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' or a - = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' - file to this project. + = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link' + to this project. - if can?(current_user, :push_code, @project) %div{ class: container_class } diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 1fe1d98bf13..9322c82904f 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -1,5 +1,4 @@ - page_title "Find File", @ref -- header_title project_title(@project, "Files", project_files_path(@project)) .file-finder-holder.tree-holder.clearfix .nav-block diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index f21c864e35c..5bc5c71283e 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -12,6 +12,9 @@ - else %strong ##{generic_commit_status.id} + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.') + - if defined?(commit_sha) && commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" @@ -37,11 +40,13 @@ %td = generic_commit_status.name - .pull-right - - if generic_commit_status.tags.any? - - generic_commit_status.tags.each do |tag| - %span.label.label-primary - = tag + %td + - if generic_commit_status.tags.any? + - generic_commit_status.tags.each do |tag| + %span.label.label-primary + = tag + - if defined?(retried) && retried + %span.label.label-warning retried %td.duration - if generic_commit_status.duration diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 79a56647c53..8becaea246f 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,3 +1,4 @@ +- page_specific_javascripts asset_path("graphs/application.js") %ul.nav-links = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path diff --git a/app/views/projects/graphs/_header_title.html.haml b/app/views/projects/graphs/_header_title.html.haml deleted file mode 100644 index 1e2f61cd22b..00000000000 --- a/app/views/projects/graphs/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Graphs", namespace_project_graph_path(@project.namespace, @project, current_ref)) diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 9f05be9982b..19ccc125ea8 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,5 +1,4 @@ - page_title "Continuous Integration", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default .oneline diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index da9f648cc9c..d9b2fb6c065 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,5 +1,4 @@ - page_title "Commits", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml index ebecab1dbfc..249c16f4709 100644 --- a/app/views/projects/graphs/languages.html.haml +++ b/app/views/projects/graphs/languages.html.haml @@ -1,5 +1,4 @@ - page_title "Languages", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index ad4a932d391..33970e7b909 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,5 +1,4 @@ - page_title "Contributors", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default @@ -19,7 +18,7 @@ .header.clearfix %h3#date_header.page-title %p.light - Commits to #{@ref}, excluding merge commits. Limited by 6,000 commits + Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. %input#brush_change{:type => "hidden"} .graphs #contributors-master diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml index 62eba5888a4..8151187d499 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 36c1d69f060..917a0b805b1 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -64,6 +64,13 @@ Build events %p.light This url will be triggered when the build status changes + %div + = f.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = f.label :wiki_page_events, class: 'label-light append-bottom-0' do + Wiki Page events + %p.light + This url will be triggered when a wiki page is created/updated .form-group = f.label :enable_ssl_verification, "SSL verification", class: "label-light" %div @@ -80,5 +87,5 @@ - @hooks.each do |hook| = render "project_hook", hook: hook - else - %p.profile-settings-message.text-center.append-bottom-0 + %p.settings-message.text-center.append-bottom-0 No webhooks found, add one in the form above. diff --git a/app/views/projects/issues/_header_title.html.haml b/app/views/projects/issues/_header_title.html.haml deleted file mode 100644 index 99f03549c44..00000000000 --- a/app/views/projects/issues/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project)) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 9ad86ed71c9..78f64150601 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,7 +6,7 @@ .issue-title.title %span.issue-title-text = confidential_icon(issue) - = link_to_gfm issue.title, issue_path(issue) + = link_to issue.title, issue_path(issue) %ul.controls - if issue.closed? %li @@ -28,16 +28,10 @@ = downvotes - note_count = issue.notes.user.nonawards.count - - if note_count > 0 - %li - = link_to issue_path(issue) + "#notes" do - = icon('comments') - = note_count - - else - %li - = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do - = icon('comments') - = note_count + %li + = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .issue-info #{issue.to_reference} · diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index d6b38b327ff..2f9dc867d0d 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -7,7 +7,7 @@ %li %span.merge-request-ci-status - if merge_request.ci_commit - = render_ci_status(merge_request.ci_commit) + = render_pipeline_status(merge_request.ci_commit) - elsif has_any_ci = icon('blank fw') %span.merge-request-id @@ -24,5 +24,8 @@ MERGED - elsif merge_request.closed? CLOSED - - if @closed_by_merge_requests.present? + %li = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} + - if @closed_by_merge_requests.present? + %li + = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index bdfa0c7009e..5f9d2919982 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -8,7 +8,7 @@ - ci_commit = @project.ci_commit(sha, branch) if sha - if ci_commit %span.related-branch-ci-status - = render_ci_status(ci_commit) + = render_pipeline_status(ci_commit) %span.related-branch-info %strong = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 20216297d25..7cf1923456e 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" -= render "header_title" %h3.page-title Edit Issue ##{@issue.iid} diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index ee8a9414657..7ad7c9c87e8 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index efa7642b2dc..19a6f4a91f6 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,5 +1,4 @@ - page_title "Issues" -= render "header_title" = content_for :meta_tags do - if current_user diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index b317a0c1cf4..e8aae0f47e2 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Issue" -= render "header_title" %h3.page-title New Issue diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bde80bbb54b..f3b0469b7d4 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,7 +1,6 @@ - page_title "#{@issue.title} (##{@issue.iid})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes -- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project)) .clearfix.detail-page-header .issuable-header @@ -53,12 +52,12 @@ .issue-details.issuable-details .detail-page-description.content-block %h2.title - = markdown escape_once(@issue.title), pipeline: :single_line + = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author - if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .wiki = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"]) + = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author) %textarea.hidden.js-task-list-field = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') diff --git a/app/views/projects/labels/_header_title.html.haml b/app/views/projects/labels/_header_title.html.haml deleted file mode 100644 index abe28da483b..00000000000 --- a/app/views/projects/labels/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Labels", namespace_project_labels_path(@project.namespace, @project)) diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 675a805e12f..6901ba13ab7 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @label.name, "Labels" -= render "header_title" %h3.page-title Edit Label diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index cc41130a9dc..2557d1a4d5b 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,5 +1,4 @@ - page_title "Labels" -= render "header_title" .top-area .nav-text @@ -18,6 +17,6 @@ - else .nothing-here-block - if can? current_user, :admin_label, @project - Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. - else No labels created diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index e20fd7d6891..49ddf901619 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Label" -= render "header_title" %h3.page-title New Label diff --git a/app/views/projects/merge_requests/_header_title.html.haml b/app/views/projects/merge_requests/_header_title.html.haml deleted file mode 100644 index 669a9b06bdf..00000000000 --- a/app/views/projects/merge_requests/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index e740fe8c84d..c02f94490a0 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,7 +1,7 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title.title %span.merge-request-title-text - = link_to_gfm merge_request.title, merge_request_path(merge_request) + = link_to merge_request.title, merge_request_path(merge_request) %ul.controls - if merge_request.merged? %li @@ -13,7 +13,7 @@ - if merge_request.ci_commit %li - = render_ci_status(merge_request.ci_commit) + = render_pipeline_status(merge_request.ci_commit) - if merge_request.open? && merge_request.broken? %li @@ -36,16 +36,10 @@ = downvotes - note_count = merge_request.mr_and_commit_notes.user.nonawards.count - - if note_count > 0 - %li - = link_to merge_request_path(merge_request) + "#notes" do - = icon('comments') - = note_count - - else - %li - = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do - = icon('comments') - = note_count + %li + = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .merge-request-info #{merge_request.to_reference} · diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 290753d57c6..7af227129ec 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,7 +1,6 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) - if diff_view == 'parallel' - fluid_layout true diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml index ba8d9a5835c..a60c445aa51 100644 --- a/app/views/projects/merge_requests/dropdowns/_branch.html.haml +++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml @@ -1,5 +1,5 @@ %ul - branches.each do |branch| %li - %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } } + %a{ href: '#', class: "#{('is-active' if selected == branch)}", title: branch, data: { id: branch } } = branch diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index b31ea5e5321..03159f123f3 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" -= render "header_title" %h3.page-title Edit Merge Request #{@merge_request.to_reference} diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index e56a44e0a79..b517e874b0f 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,5 +1,4 @@ - page_title "Merge Requests" -= render "header_title" = render 'projects/last_push' diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index f5bf16ef3ad..a00d3128ffe 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,5 +1,4 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" -= render "header_title" .merge-request = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index d259968030e..2e798ce780a 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Merge Request" -= render "header_title" - if @merge_request.can_be_created && !params[:change_branches] = render 'new_submit' diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index a23bd8d18d0..ebf18f6ac85 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,13 +1,13 @@ .detail-page-description.content-block %h2.title - = markdown escape_once(@merge_request.title), pipeline: :single_line + = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author %div - if @merge_request.description.present? .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) + = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 55dbae598d3..13359abede7 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -26,4 +26,4 @@ %i.fa.fa-check Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} = succeed '.' do - != markdown issues_sentence(@closes_issues), pipeline: :gfm + != markdown issues_sentence(@closes_issues), pipeline: :gfm, author: @merge_request.author diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 3c68d61c4b5..b79508bdc34 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,7 +13,7 @@ check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - ci_status: "", + ci_status: "#{@merge_request.ci_commit ? @merge_request.ci_commit.status : ''}", ci_message: { normal: "Build {{status}} for \"{{title}}\"", preparing: "{{status}} build for \"{{title}}\"" @@ -26,4 +26,10 @@ builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; + if (typeof merge_request_widget !== 'undefined') { + clearInterval(merge_request_widget.fetchBuildStatusInterval); + merge_request_widget.cancelPolling(); + merge_request_widget.clearEventListeners(); + } + merge_request_widget = new MergeRequestWidget(opts); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 807833741af..cfdf4edac37 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -25,7 +25,10 @@ - else = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do Accept Merge Request - - if @merge_request.can_remove_source_branch?(current_user) + - if @merge_request.force_remove_source_branch? + .accept-control + The source branch will be removed. + - elsif @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 2168294c683..b83ddcab3a4 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -2,17 +2,16 @@ Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when the build succeeds. %div - - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present? %p = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch - - if should_remove_source_branch + - if @merge_request.remove_source_branch? The source branch will be removed. - else The source branch will not be removed. - - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user + - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml index a8145558ca8..57ce1959021 100644 --- a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml @@ -1,4 +1,6 @@ -%h4 +%h4 Ready to be merged automatically %p Ask someone with write access to this repository to merge this request. + - if @merge_request.force_remove_source_branch? + The source branch will be removed. diff --git a/app/views/projects/milestones/_header_title.html.haml b/app/views/projects/milestones/_header_title.html.haml deleted file mode 100644 index 5f4b6982a6d..00000000000 --- a/app/views/projects/milestones/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Milestones", namespace_project_milestones_path(@project.namespace, @project)) diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 43f8863163d..be682226ab6 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @milestone.title, "Milestones" -= render "header_title" %h3.page-title Edit Milestone ##{@milestone.iid} diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index abe567af1dd..e6133b22f96 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,6 +1,4 @@ - page_title "Milestones" -= render "header_title" - .top-area = render 'shared/milestones_filter' diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 0d016f78313..7f372b41698 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Milestone" -= render "header_title" %h3.page-title New Milestone diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 6ec84660157..19944e3e023 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,8 +1,6 @@ - page_title @milestone.title, "Milestones" - page_description @milestone.description -= render "header_title" - .detail-page-header .status-box{ class: status_box_class(@milestone) } - if @milestone.closed? diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 8065663ca2a..326180ebe4e 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,5 +1,4 @@ - page_title "Network", @ref -= render "projects/commits/header_title" = render "projects/commits/head" = render "head" .project-network diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a4c6094c69a..f9ac16b32f3 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,5 +1,5 @@ - page_title 'New Project' -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path %h3.page-title New Project diff --git a/app/views/projects/notes/_commit_discussion.html.haml b/app/views/projects/notes/_commit_discussion.html.haml deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 39be072855a..8144c1ba49e 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -1,10 +1,8 @@ -- note = notes.first # example note --# Check if line want not changed since comment was left -- if !defined?(line) || line == note.diff_line - %tr.notes_holder - %td.notes_line{ colspan: 2 } - %td.notes_content - %ul.notes{ data: { discussion_id: note.discussion_id } } - = render notes - .discussion-reply-holder - = link_to_reply_diff(note) +- note = notes.first +%tr.notes_holder + %td.notes_line{ colspan: 2 } + %td.notes_content + %ul.notes{ data: { discussion_id: note.discussion_id } } + = render partial: "projects/notes/note", collection: notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index f8aa5e2fa7d..45986b0d1e8 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -1,27 +1,27 @@ -- note1 = notes_left.present? ? notes_left.first : nil -- note2 = notes_right.present? ? notes_right.first : nil +- note_left = notes_left.present? ? notes_left.first : nil +- note_right = notes_right.present? ? notes_right.first : nil %tr.notes_holder - - if note1 + - if note_left %td.notes_line.old %td.notes_content.parallel.old - %ul.notes{ data: { discussion_id: note1.discussion_id } } - = render notes_left + %ul.notes{ data: { discussion_id: note_left.discussion_id } } + = render partial: "projects/notes/note", collection: notes_left, as: :note .discussion-reply-holder - = link_to_reply_diff(note1, 'old') + = link_to_reply_discussion(note_left, 'old') - else %td.notes_line.old= "" %td.notes_content.parallel.old= "" - - if note2 + - if note_right %td.notes_line.new %td.notes_content.parallel.new - %ul.notes{ data: { discussion_id: note2.discussion_id } } - = render notes_right + %ul.notes{ data: { discussion_id: note_right.discussion_id } } + = render partial: "projects/notes/note", collection: notes_right, as: :note .discussion-reply-holder - = link_to_reply_diff(note2, 'new') + = link_to_reply_discussion(note_right, 'new') - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 572b00a38c7..7869d6413d8 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,13 +1,46 @@ - note = discussion_notes.first +- expanded = !note.diff_note? || note.active? %li.note.note-discussion.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = image_tag avatar_icon(note.author), class: "avatar s40" .timeline-content - - if note.for_merge_request? - - (active_notes, outdated_notes) = discussion_notes.partition(&:active?) - = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0 - = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0 - - else - = render "projects/notes/discussions/commit", discussion_notes: discussion_notes + .discussion.js-toggle-container{ class: note.discussion_id } + .discussion-header + = link_to_member(@project, note.author, avatar: false) + + .inline.discussion-headline-light + = note.author.to_reference + started a discussion on + + - if note.for_commit? + - commit = note.noteable + - if commit + commit + = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace' + - else + a deleted commit + - else + - if note.active? + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + the diff + - else + an outdated diff + + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") + + .discussion-actions + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + Toggle discussion + + .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } + - if note.diff_note? + = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes + - else + = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index d0ac380f216..67ed38a7b22 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -6,6 +6,7 @@ = f.hidden_field :line_code = f.hidden_field :noteable_id = f.hidden_field :noteable_type + = f.hidden_field :type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index aeb7c1d5ee4..f1045bbd8c3 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,5 +1,8 @@ +- return unless note.author +- return if note.cross_reference_not_visible_for?(current_user) + - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} @@ -8,8 +11,8 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference}" - - if !note.system + = note.author.to_reference + - unless note.system commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') @@ -26,7 +29,7 @@ .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text = preserve do - = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) + = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) - if note_editable = render 'projects/notes/edit_form', note: note = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index 62db86fb181..ebf7e8a9cb3 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -2,14 +2,9 @@ - @discussions.each do |discussion_notes| - note = discussion_notes.first - if note_for_main_target?(note) - - next if note.cross_reference_not_visible_for?(current_user) - - = render discussion_notes + = render partial: "projects/notes/note", object: note, as: :note - else = render 'projects/notes/discussion', discussion_notes: discussion_notes - else - @notes.each do |note| - - next unless note.author - - next if note.cross_reference_not_visible_for?(current_user) - - = render note + = render partial: "projects/notes/note", object: note, as: :note diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml deleted file mode 100644 index 0ea8862a684..00000000000 --- a/app/views/projects/notes/discussions/_active.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - on the diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - - .discussion-body.js-toggle-content - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml deleted file mode 100644 index 2a2ead58eeb..00000000000 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- note = discussion_notes.first -- commit = note.noteable -- commit_description = commit ? 'commit' : 'a deleted commit' -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion on #{commit_description}" - - if commit - = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - .discussion-body.js-toggle-content - - if note.for_diff_line? - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - - else - .panel.panel-default - .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } - %ul.notes.timeline - = render discussion_notes - .discussion-reply-holder - = link_to_reply_diff(discussion_notes.first) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml deleted file mode 100644 index d46aab000c3..00000000000 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- diff = note.diff -- if diff - .diff-file - .diff-header - %span - - if diff.deleted_file - = diff.old_path - - else - = diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - .diff-content.code.js-syntax-highlight - %table - - note.truncated_diff_lines.each do |line| - - type = line.type - - line_code = generate_line_code(note.file_path, line) - %tr.line_holder{ id: line_code, class: "#{type}" } - - if type == "match" - %td.old_line.diff-line-num= "..." - %td.new_line.diff-line-num= "..." - %td.line_content.match= line.text - - else - %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } - %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } - %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - - - if line_code == note.line_code - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml new file mode 100644 index 00000000000..6401245bf73 --- /dev/null +++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml @@ -0,0 +1,30 @@ +- note = discussion_notes.first +- diff = note.diff +- return unless diff + +.diff-file + .diff-header + %span + - if diff.deleted_file + = diff.old_path + - else + = diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" + .diff-content.code.js-syntax-highlight + %table + - note.truncated_diff_lines.each do |line| + - type = line.type + - line_code = generate_line_code(note.diff_file_path, line) + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" + %td.old_line.diff-line-num= "..." + %td.new_line.diff-line-num= "..." + %td.line_content.match= line.text + - else + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) + + - if line_code == note.line_code + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml new file mode 100644 index 00000000000..e598e3c7c63 --- /dev/null +++ b/app/views/projects/notes/discussions/_notes.html.haml @@ -0,0 +1,7 @@ +- note = discussion_notes.first +.panel.panel-default + .notes{ data: { discussion_id: note.discussion_id } } + %ul.notes.timeline + = render partial: "projects/notes/note", collection: discussion_notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml deleted file mode 100644 index 45141bcd1df..00000000000 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - on the outdated diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-down - Show/hide discussion - .discussion-body.js-toggle-content.hide - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml new file mode 100644 index 00000000000..6e757df5417 --- /dev/null +++ b/app/views/projects/pipelines/_head.html.haml @@ -0,0 +1,14 @@ +%ul.nav-links + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) + + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml new file mode 100644 index 00000000000..8289aefcde7 --- /dev/null +++ b/app/views/projects/pipelines/_info.html.haml @@ -0,0 +1,37 @@ +%p +.commit-info-row + Pipeline + = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace" + with + = pluralize @pipeline.statuses.count(:id), "build" + - if @pipeline.ref + for + = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" + - if @pipeline.duration + in + = time_interval_in_words @pipeline.duration + + .pull-right + = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do + = ci_icon_for_status(@pipeline.status) + = ci_label_for_status(@pipeline.status) + +- if @commit + .commit-info-row + %span.light Authored by + %strong + = commit_author_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.authored_date)} + +.commit-info-row + %span.light Commit + = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" + = clipboard_button(clipboard_text: @pipeline.sha) + +- if @commit + .commit-box.content-block + %h3.commit-title + = markdown escape_once(@commit.title), pipeline: :single_line + - if @commit.description.present? + %pre.commit-description + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml new file mode 100644 index 00000000000..453767920b5 --- /dev/null +++ b/app/views/projects/pipelines/index.html.haml @@ -0,0 +1,58 @@ +- page_title "Pipelines" += render "projects/pipelines/head" + +.top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_pipelines_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@pipelines_count) + + %li{class: ('active' if @scope == 'running')} + = link_to project_pipelines_path(@project, scope: :running) do + Running + %span.badge.js-running-count + = number_with_delimiter(@running_or_pending_count) + + %li{class: ('active' if @scope == 'branches')} + = link_to project_pipelines_path(@project, scope: :branches) do + Branches + + %li{class: ('active' if @scope == 'tags')} + = link_to project_pipelines_path(@project, scope: :tags) do + Tags + + .nav-controls + - if can? current_user, :create_pipeline, @project + = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do + = icon('plus') + New pipeline + + - unless @repository.gitlab_ci_yml + = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + + = link_to ci_lint_path, class: 'btn btn-default' do + = icon('wrench') + %span CI Lint + +%ul.content-list.pipelines + - stages = @pipelines.stages + - if @pipelines.blank? + %li + .nothing-here-block No pipelines to show + - else + .table-holder + %table.table.builds + %tbody + %th ID + %th Commit + - stages.each do |stage| + %th.stage + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize.pluralize + %th Duration + %th + = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + + = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml new file mode 100644 index 00000000000..5f4ec2e40c8 --- /dev/null +++ b/app/views/projects/pipelines/new.html.haml @@ -0,0 +1,21 @@ +- page_title "New Pipeline" + +%h3.page-title + New Pipeline +%hr + += form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f| + = form_errors(@pipeline) + .form-group + = f.label :ref, 'Create for', class: 'control-label' + .col-sm-10 + = f.text_field :ref, required: true, tabindex: 2, class: 'form-control' + .help-block Existing branch name, tag + .form-actions + = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 + = link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel' + +:javascript + var availableRefs = #{@project.repository.ref_names.to_json}; + + new NewBranchForm($('.js-new-pipeline-form'), availableRefs) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml new file mode 100644 index 00000000000..2aad5602414 --- /dev/null +++ b/app/views/projects/pipelines/show.html.haml @@ -0,0 +1,8 @@ +- page_title "Pipeline" + +.prepend-top-default + - if @commit + = render "projects/pipelines/info" + %div.block-connector + += render "projects/commit/ci_commit", ci_commit: @pipeline diff --git a/app/views/projects/project_members/_header_title.html.haml b/app/views/projects/project_members/_header_title.html.haml deleted file mode 100644 index a31f0a37fa2..00000000000 --- a/app/views/projects/project_members/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Members", namespace_project_project_members_path(@project.namespace, @project)) diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 189906498cb..eef97107d77 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,5 +1,4 @@ - page_title "Import members" -= render "header_title" %h3.page-title Import members from another project diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index ebcfc907ebb..15dc064e7ea 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,5 +1,4 @@ - page_title "Members" -= render "header_title" .project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index b9e9dd8aaea..565905cbe7b 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,7 +1,7 @@ %h5.prepend-top-0 Already Protected (#{@branches.size}) - if @branches.empty? - %p.profile-settings-message.text-center + %p.settings-message.text-center No branches are protected, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 0d59cec322c..835398b6f98 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @tag.name, "Tags" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index 6ca919f7f80..43a6fdfd103 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line + = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml new file mode 100644 index 00000000000..d62f5c8f131 --- /dev/null +++ b/app/views/projects/runners/_form.html.haml @@ -0,0 +1,32 @@ += form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| + = form_errors(runner) + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused runners don't accept new builds + .form-group + = label :run_untagged, 'Run untagged jobs', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :run_untagged + %span.light Indicates whether this runner can pick jobs without tags + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control' + .help-block You can setup jobs to only use runners with specific tags + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 47ec420189d..96e2aac451f 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -5,7 +5,7 @@ - if @runners.include?(runner) = link_to runner.short_sha, runner_path(runner) %small - =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do + = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do %i.fa.fa-edit.btn - else = runner.short_sha diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 30cd1263a12..8ae9f0d95f7 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -8,7 +8,7 @@ Install GitLab Runner software. Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it %li - Specify following URL during runner setup: + Specify the following URL during runner setup: %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index eba03028af8..95706888655 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,29 +1,6 @@ - page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" %h4 Runner ##{@runner.id} + %hr -= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label :active, "Active", class: 'control-label' - .col-sm-10 - .checkbox - = f.check_box :active - %span.light Paused runners don't accept new builds - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup jobs to only use runners with specific tags - .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = render 'form', runner: @runner, runner_form_url: runner_path(@runner) diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index 5bf4c09ca25..f24e1b9144e 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -17,50 +17,39 @@ %th Property Name %th Value %tr - %td - Tags + %td Active + %td= @runner.active? ? 'Yes' : 'No' + %tr + %td Can run untagged jobs + %td= @runner.run_untagged? ? 'Yes' : 'No' + %tr + %td Tags %td - @runner.tag_list.each do |tag| %span.label.label-primary = tag %tr - %td - Name - %td - = @runner.name + %td Name + %td= @runner.name %tr - %td - Version - %td - = @runner.version + %td Version + %td= @runner.version %tr - %td - Revision - %td - = @runner.revision + %td Revision + %td= @runner.revision %tr - %td - Platform - %td - = @runner.platform + %td Platform + %td= @runner.platform %tr - %td - Architecture - %td - = @runner.architecture + %td Architecture + %td= @runner.architecture %tr - %td - Description - %td - = @runner.description + %td Description + %td= @runner.description %tr - %td - Last contact + %td Last contact %td - if @runner.contacted_at #{time_ago_in_words(@runner.contacted_at)} ago - else Never - - - diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 1b70880043a..1f13ea28b4e 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -1,18 +1,16 @@ -%h3.page-title - = @service.title - = boolean_to_icon @service.activated? +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = @service.title + = boolean_to_icon @service.activated? -%p= @service.description - -%hr - -= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| - = render 'shared/service_settings', form: form - - .form-actions - = form.submit 'Save changes', class: 'btn btn-save' -   - - if @service.valid? && @service.activated? - - disabled = @service.can_test? ? '':'disabled' - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" + %p= @service.description + .col-lg-9 + = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| + = render 'shared/service_settings', form: form + = form.submit 'Save changes', class: 'btn btn-save' +   + - if @service.valid? && @service.activated? + - disabled = @service.can_test? ? '':'disabled' + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index c1356f6db02..4a33a5bc6f6 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -1,24 +1,32 @@ - page_title "Services" -%h3.page-title Project services -%p.light Project services allow you to integrate GitLab with other applications -.table-holder - %table.table - %thead - %tr - %th - %th Service - %th Description - %th Last edit - - @services.sort_by(&:title).each do |service| - %tr - %td - = boolean_to_icon service.activated? - %td - = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do - %strong= service.title - %td - = service.description - %td.light - = time_ago_in_words service.updated_at - ago +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Project services + %p Project services allow you to integrate GitLab with other applications + .col-lg-9 + %table.table + %colgroup + %col + %col + %col.hidden-xs + %col{ width: "120" } + %thead + %tr + %th + %th Service + %th.hidden-xs Description + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = boolean_to_icon service.activated? + %td + = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do + %strong= service.title + %td.hidden-xs + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 9b3d3f069d9..11310d5e1e1 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_url(@project.namespace, @project) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 74feb9e3282..a19c7c406a0 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,50 +13,50 @@ = render "home_panel" .project-stats.row-content-block.second-block - %ul.nav - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - = pluralize(number_with_delimiter(@project.commit_count), 'commit') - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - = pluralize(number_with_delimiter(@repository.branch_names.count), 'branch') - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag') - - %li - = link_to project_files_path(@project) do - = repository_size - - - if default_project_view != 'readme' && @repository.readme + .container-fluid.container-limited + %ul.nav %li - = link_to 'Readme', readme_path(@project) - - - if @repository.changelog + = link_to project_files_path(@project) do + Files (#{repository_size}) %li - = link_to 'Changelog', changelog_path(@project) - - - if @repository.license_blob + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_names.count)} (#{number_with_delimiter(@repository.branch_names.count)}) %li - = link_to 'Contribution guide', contribution_guide_path(@project) + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_names.count)} (#{number_with_delimiter(@repository.tag_names.count)}) + + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) + + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) + + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide - if @repository.commit .content-block.second-block.white diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 4a515469422..bf57beb9d07 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,11 +1,27 @@ -= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do - = icon('plus') - New Snippet -- if can?(current_user, :admin_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do - = icon('trash-o') - Delete -- if can?(current_user, :update_project_snippet, @snippet) - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do - = icon('pencil-square-o') - Edit +.hidden-xs + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do + = icon('plus') + New Snippet + - if can?(current_user, :update_project_snippet, @snippet) + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + Edit + - if can?(current_user, :update_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + Delete +.visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-full-width + %ul + %li + = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do + New Snippet + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do + Edit + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete diff --git a/app/views/projects/snippets/_header_title.html.haml b/app/views/projects/snippets/_header_title.html.haml deleted file mode 100644 index 04f0bbe9853..00000000000 --- a/app/views/projects/snippets/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Snippets", namespace_project_snippets_path(@project.namespace, @project)) diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index dc3ea1fcf12..216f70f5605 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @snippet.title, "Snippets" -= render "header_title" %h3.page-title Edit Snippet diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 103ff447464..96fee3b17b2 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,5 +1,4 @@ - page_title "Snippets" -= render "header_title" .row-content-block.top-block .pull-right diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index e57237991b4..772a594269c 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Snippets" -= render "header_title" %h3.page-title New Snippet diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 7c599563ce4..bae4d8f349f 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,18 +1,15 @@ - page_title @snippet.title, "Snippets" -= render "header_title" .snippet-holder = render 'shared/snippets/header' - %article.file-holder - .file-title + %article.file-holder.file-holder-no-border.snippet-file-content + .file-title.file-title-clear = blob_icon 0, @snippet.file_name - %strong - = @snippet.file_name + = @snippet.file_name .file-actions.hidden-xs = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml index ffeacb5a004..e4a78fadbeb 100644 --- a/app/views/projects/tags/destroy.js.haml +++ b/app/views/projects/tags/destroy.js.haml @@ -1,3 +1,2 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}"); - if @repository.tags.empty? $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index dc6ece30dd2..8f381663e6e 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,5 +1,4 @@ - page_title "Tags" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index b40a6e5cb2d..3a097750d6e 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Tag" -= render "projects/commits/header_title" - if @error .alert.alert-danger @@ -23,7 +22,7 @@ .form-group = label_tag :message, nil, class: 'control-label' .col-sm-10 - = text_field_tag :message, nil, required: false, tabindex: 3, class: 'form-control' + = text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5 .help-block Optionally, enter a message to create an annotated tag. %hr .form-group diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 9c916fd02de..b7d7d5c5382 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -1,5 +1,4 @@ - page_title @tag.name, "Tags" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block @@ -19,15 +18,13 @@ %i.fa.fa-trash-o .title %span.item-title= @tag.name - - if @tag.message.present? - %span.light -   - = strip_gpg_signature(@tag.message) - if @commit = render 'projects/branches/commit', commit: @commit, project: @project - else Cant find HEAD commit for this tag - + - if @tag.message.present? + %pre.body + = strip_gpg_signature(@tag.message) .append-bottom-default.prepend-top-default - if @release.description.present? diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 91fb2a44594..59f60c4687c 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,9 +1,9 @@ - page_title @path.presence || "Files", @ref -- header_title project_title(@project, "Files", project_files_path(@project)) = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' += render "projects/commits/head" .tree-controls = render 'projects/find_file_link' diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index f91885b216d..7f3de47d7df 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -5,7 +5,7 @@ %h4.prepend-top-0 = page_title %p - Triggers can be used to force a rebuild of a specific branch or tag with an API call. + Triggers can force a specific branch or tag to rebuild with an API call. .col-lg-9 %h5.prepend-top-0 Your triggers @@ -18,8 +18,8 @@ %th = render partial: 'trigger', collection: @triggers, as: :trigger - else - %p.profile-settings-message.text-center.append-bottom-default - There are no triggers to use, add one by the button below. + %p.settings-message.text-center.append-bottom-default + No triggers have been created yet. Add one using the button below. = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| = f.submit "Add Trigger", class: 'btn btn-success' @@ -28,8 +28,7 @@ Use CURL %p.light - Copy the token above and set your branch or tag name. This is the reference that will be rebuild. - + Copy the token above, set your branch or tag name, and that reference will be rebuilt. %pre :plain @@ -41,10 +40,10 @@ Use .gitlab-ci.yml %p.light - Copy the snippet to - %i .gitlab-ci.yml - of dependent project. - At the end of your build it will trigger this project to rebuilt. + In the + %code .gitlab-ci.yml + of the dependent project, include the following snippet. + The project will rebuild at the end of the build. %pre :plain @@ -57,9 +56,8 @@ %p.light Add - %strong variables[VARIABLE]=VALUE - to API request. - The value of variable could then be used to distinguish triggered build from normal one. + %code variables[VARIABLE]=VALUE + to an API request. Variable values can be used to distinguish between triggered builds and normal builds. %pre.append-bottom-0 :plain diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml new file mode 100644 index 00000000000..0249e0c1bf1 --- /dev/null +++ b/app/views/projects/variables/_content.html.haml @@ -0,0 +1,8 @@ +%h4.prepend-top-0 + Secret Variables +%p + These variables will be set to environment by the runner. +%p + So you can use them for passwords, secret keys or whatever you want. +%p + The value of the variable can be visible in build log if explicitly asked to do so. diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml new file mode 100644 index 00000000000..a5bae83e0ce --- /dev/null +++ b/app/views/projects/variables/_form.html.haml @@ -0,0 +1,10 @@ += form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f| + = form_errors(@variable) + + .form-group + = f.label :key, "Key", class: "label-light" + = f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true + .form-group + = f.label :value, "Value", class: "label-light" + = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true + = f.submit btn_text, class: "btn btn-save" diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml new file mode 100644 index 00000000000..6c43f822db4 --- /dev/null +++ b/app/views/projects/variables/_table.html.haml @@ -0,0 +1,25 @@ +.table-responsive.variables-table + %table.table + %colgroup + %col + %col + %col{ width: 100 } + %thead + %th Key + %th Value + %th + %tbody + - @project.variables.each do |variable| + - if variable.id? + %tr + %td= variable.key + %td= variable.value + %td + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do + %span.sr-only + Update + = icon("pencil") + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do + %span.sr-only + Remove + = icon("trash") diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml new file mode 100644 index 00000000000..09bb54600af --- /dev/null +++ b/app/views/projects/variables/index.html.haml @@ -0,0 +1,17 @@ +- page_title "Variables" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h5.prepend-top-0 + Add a variable + = render "form", btn_text: "Add new variable" + %hr + %h5.prepend-top-0 + Your variables (#{@project.variables.size}) + - if @project.variables.empty? + %p.settings-message.text-center.append-bottom-0 + No variables found, add one with the form above. + - else + = render "table" diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index ca284b84d39..297a53ca98c 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -1,36 +1,9 @@ - page_title "Variables" -%h3.page-title - Secret Variables -%p.light - These variables will be set to environment by the runner. - %br - So you can use them for passwords, secret keys or whatever you want. - %br - The value of the variable can be visible in build log if explicitly asked to do so. - -%hr - - -= nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - = form_errors(@project) - - = f.fields_for :variables do |variable_form| - .form-group - = variable_form.label :key, 'Key', class: 'control-label' - .col-sm-10 - = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE" - - .form-group - = variable_form.label :value, 'Value', class: 'control-label' - .col-sm-10 - = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: "" - - = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10' - %hr - %p - .clearfix - = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' - - .form-actions - = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h5.prepend-top-0 + Update variable + = render "form", btn_text: "Save variable" diff --git a/app/views/projects/wikis/_header_title.html.haml b/app/views/projects/wikis/_header_title.html.haml deleted file mode 100644 index 408adc36ca6..00000000000 --- a/app/views/projects/wikis/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, 'Wiki', get_project_wiki_path(@project)) diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 4dd818c7f67..aaa15dd3bbe 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @page.title.capitalize, "Wiki" -= render "header_title" = render 'nav' .top-area diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index c7e490c3cd1..7dfa405d063 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,5 +1,4 @@ - page_title "Wiki" -= render "header_title" %h3.page-title Empty page %hr diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index ba3f2cadc48..ccceab6155e 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,5 +1,4 @@ - page_title "Git Access", "Wiki" -= render "header_title" = render 'nav' .row-content-block diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index dcaddae2b04..45460ed9f41 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,5 +1,4 @@ - page_title "History", @page.title.capitalize, "Wiki" -= render "header_title" = render 'nav' .top-area diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 92b494a513c..2f6162fa3c5 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,5 +1,4 @@ - page_title "Pages", "Wiki" -= render "header_title" = render 'nav' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 067fb7f8f54..1cb48a1e85d 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,5 +1,4 @@ - page_title @page.title.capitalize, "Wiki" -= render "header_title" = render 'nav' .top-area diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 640890fbe92..8f68d6d1b87 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -7,7 +7,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project })) + = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author })) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 333f6533213..6331c2bd6b0 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -6,7 +6,7 @@ - if merge_request.description.present? .description.term = preserve do - = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project })) + = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author })) %span.light #{merge_request.project.name_with_namespace} .pull-right diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index d9400b1d9fa..8163aff43b6 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -19,4 +19,4 @@ .note-search-result .term = preserve do - = search_md_sanitize(markdown(note.note, {no_header_anchors: true})) + = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author})) diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 974751d9970..84b3f44c0ad 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -5,7 +5,7 @@ %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'} %span = default_clone_protocol.upcase - = icon('angle-down') + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown %li = ssh_clone_button(project) diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index c38d9313dba..30055002213 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,5 +1,7 @@ -%ul.nav-links.event-filter +%ul.nav-links.event-filter.scrolling-tabs + .fade-left = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' + .fade-right diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index d327bd0a96f..1e0f075b303 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -6,7 +6,7 @@ - else = sort_title_recently_created %b.caret - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to page_filter_path(sort: sort_value_recently_created) do = sort_title_recently_created diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml index 1aa7ed1f2eb..427595c47a5 100644 --- a/app/views/shared/groups/_list.html.haml +++ b/app/views/shared/groups/_list.html.haml @@ -3,4 +3,4 @@ - groups.each_with_index do |group, i| = render "shared/groups/group", group: group - else - %h3 No groups found + .nothing-here-block No groups found diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 9474462cbd1..cedff4af2e0 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,6 +1,8 @@ .issues-filters .issues-details-filters.row-content-block.second-block - = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:issue_search].present? + = hidden_field_tag :issue_search, params[:issue_search] - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .check-all-holder = check_box_tag "check_all_issues", nil, false, @@ -10,7 +12,7 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5c52cc6d1da..b430251dbf6 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -44,45 +44,53 @@ This issue is confidential and should only be visible to team members - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + - has_due_date = issuable.has_attribute?(:due_date) %hr - .form-group - .issue-assignee - = f.label :assignee_id, "Assignee", class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) -   - = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' - .form-group - .issue-milestone - = f.label :milestone_id, "Milestone", class: 'control-label' - .col-sm-10 - - if milestone_options(issuable).present? + .row + %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } + .form-group.issue-assignee + = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - - else - .prepend-top-10 - %span.light No open milestones available. -   - - if can? current_user, :admin_milestone, issuable.project - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank - .form-group - - has_labels = issuable.project.labels.any? - = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } - - if has_labels - .issuable-form-select-holder - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - - else - %span.light No labels yet. -   - - if can? current_user, :admin_label, issuable.project - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true, include_blank: true) + %div + = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + .form-group.issue-milestone + = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + - if milestone_options(issuable).present? + .issuable-form-select-holder + = f.select(:milestone_id, milestone_options(issuable), + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) + - else + .prepend-top-10 + %span.light No open milestones available. + - if can? current_user, :admin_milestone, issuable.project + %div + = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .form-group + - has_labels = issuable.project.labels.any? + = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + - if has_labels + .issuable-form-select-holder + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } + - else + %span.light No labels yet. + - if can? current_user, :admin_label, issuable.project + %div + = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + - if has_due_date + .col-lg-6 + .form-group + = f.label :due_date, "Due date", class: "control-label" + = f.hidden_field :due_date, id: "issuable-due-date" + .col-sm-10 + .datepicker - if issuable.can_move?(current_user) %hr @@ -90,9 +98,7 @@ = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 .issuable-form-select-holder - - projects = project_options(issuable, current_user, ability: :admin_issue) - = select_tag(:move_to_project_id, projects, include_blank: true, - class: 'select2', data: { placeholder: 'Select project' }) + = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }   %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } @@ -114,6 +120,13 @@ - if @merge_request.new_record?   = link_to 'Change branches', mr_change_branches_path(@merge_request) + - if @merge_request.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? + Remove source branch when merge request is accepted. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index afad48499b7..186963b32b8 100644 --- a/app/views/shared/issuable/_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,8 +1,2 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false } - = hidden_field_tag :state, params['state'] - = hidden_field_tag :scope, params['scope'] - = hidden_field_tag :assignee_id, params['assignee_id'] - = hidden_field_tag :author_id, params['author_id'] - = hidden_field_tag :milestone_id, params['milestone_id'] - = hidden_field_tag :label_id, params['label_id'] diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ed1b8a8da2a..d6552ae7f18 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -87,10 +87,16 @@ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - - if issuable.due_date - = issuable.due_date.to_s(:medium) - - else - .light None + %span.value-content + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + %span.light.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } + \- + %a.js-remove-due-date{ href: "#", role: "button" } + remove due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .selectbox.hide-collapsed = f.hidden_field :due_date, value: issuable.due_date @@ -108,20 +114,20 @@ .sidebar-collapsed-icon = icon('tags') %span - = issuable.labels.count + = issuable.labels_array.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } - - if issuable.labels.any? - - issuable.labels.each do |label| + .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + - if issuable.labels_array.any? + - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else .light None .selectbox.hide-collapsed - - issuable.labels.each do |label| + - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml index 67ae85ac276..549d2e2f61e 100644 --- a/app/views/shared/milestones/_participants_tab.html.haml +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -3,6 +3,6 @@ %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) + %strong= truncate(user.name, length: 40) %br %small.cgray= user.username diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index ab8b022411d..b8b66d08db8 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,12 +12,9 @@ %li.project-row{ class: css_class } = cache(cache_key) do .controls - - if project.main_language - %span - = project.main_language - if project.commit.try(:status) %span - = render_ci_status(project.commit) + = render_commit_status(project.commit) - if forks %span = icon('code-fork') diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index e65b1814872..af753496260 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,25 +1,24 @@ -.detail-page-header - .snippet-box.has-tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} +.detail-page-header.clearfix + .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } } + %span.sr-only + = visibility_level_label(@snippet.visibility_level) = visibility_level_icon(@snippet.visibility_level, fw: false) - = visibility_level_label(@snippet.visibility_level) - %span.identifier - Snippet ##{@snippet.id} + %strong.item-title + Snippet #{@snippet.to_reference} %span.creator - · created by #{link_to_member(@project, @snippet.author, size: 24)} - · + created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")} = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - if @snippet.updated_at != @snippet.created_at %span - · = icon('edit', title: 'edited') = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') - .pull-right + .snippet-actions - if @snippet.project_id? = render "projects/snippets/actions" - else = render "snippets/actions" -.detail-page-description.row-content-block.second-block - %h2.title - = markdown escape_once(@snippet.title), pipeline: :single_line +.content-block.second-block + %h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 1979ae6d5bc..a7769654b61 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,11 +1,27 @@ -= link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do - = icon('plus') - New Snippet -- if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do - = icon('pencil-square-o') - Edit -- if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do - = icon('trash-o') - Delete +.hidden-xs + = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do + = icon('plus') + New Snippet + - if can?(current_user, :update_personal_snippet, @snippet) + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + Delete +.visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-full-width + %ul + %li + = link_to new_snippet_path, title: "New Snippet" do + New Snippet + - if can?(current_user, :update_personal_snippet, @snippet) + %li + = link_to edit_snippet_path(@snippet) do + Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + %li + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index a2b36568770..ed3992650d4 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -3,11 +3,10 @@ .snippet-holder = render 'shared/snippets/header' - %article.file-holder - .file-title + %article.file-holder.file-holder-no-border.snippet-file-content + .file-title.file-title-clear = blob_icon 0, @snippet.file_name - %strong - = @snippet.file_name + = @snippet.file_name .file-actions.hidden-xs = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 1de71f37d1a..77f2ddefb1e 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,10 +1,9 @@ -#cal-heatmap.calendar - :javascript - new Calendar( - #{@timestamps.to_json}, - #{@starting_year}, - #{@starting_month}, - '#{user_calendar_activities_path}' - ); - -.calendar-hint Summary of issues, merge requests, and push events +.clearfix.calendar + .js-contrib-calendar + .calendar-hint + Summary of issues, merge requests, and push events +:javascript + new Calendar( + #{@timestamps.to_json}, + '#{user_calendar_activities_path}' + ); diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 027a93a75fc..630d97e339d 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,23 +1,27 @@ %h4.prepend-top-20 - %span.light Contributions for + Contributions for %strong #{@calendar_date.to_s(:short)} -%ul.bordered-list - - @events.sort_by(&:created_at).each do |event| - %li - %span.light - %i.fa.fa-clock-o - = event.created_at.to_s(:time) - - if event.push? - #{event.action_name} #{event.ref_type} #{event.ref_name} - - else - = event_action_name(event) - - if event.target - %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] - - at - %strong - - if event.project - = link_to_project event.project +- if @events.any? + %ul.bordered-list + - @events.sort_by(&:created_at).each do |event| + %li + %span.light + %i.fa.fa-clock-o + = event.created_at.to_s(:time) + - if event.push? + #{event.action_name} #{event.ref_type} #{event.ref_name} - else - = event.project_name + = event_action_name(event) + - if event.target + %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] + + at + %strong + - if event.project + = link_to_project event.project + - else + = event.project_name +- else + %p + No contributions found for #{@calendar_date.to_s(:short)} diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index e9e466c6350..6c85e5f9fbd 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id user_url(@user) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3c0b89c6741..8268380dafc 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,6 @@ - page_title @user.name - page_description @user.bio +- page_specific_javascripts asset_path("users/application.js") - header_title @user.name, user_path(@user) - @no_container = true @@ -81,15 +82,17 @@ %li.projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects + %li.snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - %div{ class: container_class } - .user-calendar{data: {href: user_calendar_path}} - %h4.center.light - %i.fa.fa-spinner.fa-spin + .user-calendar{data: {href: user_calendar_path}} + %h4.center.light + %i.fa.fa-spinner.fa-spin .user-calendar-activities .content_list{ data: {href: user_path} } @@ -104,6 +107,9 @@ #projects.tab-pane - # This tab is always loaded via AJAX + #snippets.tab-pane + - # This tab is always loaded via AJAX + .loading-status = spinner diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index c4d8595d45d..971f969e25e 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,6 +1,9 @@ class EmailsOnPushWorker include Sidekiq::Worker + sidekiq_options queue: :mailers + attr_reader :email, :skip_premailer + def perform(project_id, recipients, push_data, options = {}) options.symbolize_keys! options.reverse_merge!( @@ -25,15 +28,18 @@ class EmailsOnPushWorker :push end + diff_refs = nil compare = nil reverse_compare = false if action == :push compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) + diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)] return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)] reverse_compare = true @@ -41,26 +47,42 @@ class EmailsOnPushWorker end end - recipients.split(" ").each do |recipient| + recipients.split.each do |recipient| begin - Notify.repository_push_email( - project_id, + send_email( recipient, - author_id: author_id, - ref: ref, - action: action, - compare: compare, - reverse_compare: reverse_compare, - send_from_committer_email: send_from_committer_email, - disable_diffs: disable_diffs - ).deliver_now + project_id, + author_id: author_id, + ref: ref, + action: action, + compare: compare, + reverse_compare: reverse_compare, + diff_refs: diff_refs, + send_from_committer_email: send_from_committer_email, + disable_diffs: disable_diffs + ) + # These are input errors and won't be corrected even if Sidekiq retries rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") end end ensure + @email = nil compare = nil GC.start end + + private + + def send_email(recipient, project_id, options) + # Generating the body of this email can be expensive, so only do it once + @skip_premailer ||= email.present? + @email ||= Notify.repository_push_email(project_id, options) + + email.to = recipient + email.add_message_id + email.header[:skip_premailer] = true if skip_premailer + email.deliver_now + end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f9e32337983..d947f105516 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,8 +15,7 @@ class RepositoryForkWorker result = gitlab_shell.fork_repository(source_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") - project.update(import_error: "The project could not be forked.") - project.import_fail + project.mark_import_as_failed('The project could not be forked.') return end @@ -24,8 +23,7 @@ class RepositoryForkWorker unless project.valid_repo? logger.error("Project #{project_id} had an invalid repository after fork") - project.update(import_error: "The forked repository is invalid.") - project.import_fail + project.mark_import_as_failed('The forked repository is invalid.') return end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 2937493c614..7d819fe78f8 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,8 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.update(import_error: result[:message]) - project.import_fail + project.mark_import_as_failed(result[:message]) return end diff --git a/config/application.rb b/config/application.rb index b602e2b6168..49d4d3ba555 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,23 +1,32 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' -require 'devise' -I18n.config.enforce_available_locales = false + Bundler.require(:default, Rails.env) -require_relative '../lib/gitlab/redis' module Gitlab class Application < Rails::Application + require_dependency Rails.root.join('lib/gitlab/redis') + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths.push(*%W(#{config.root}/lib - #{config.root}/app/models/hooks - #{config.root}/app/models/concerns - #{config.root}/app/models/project_services - #{config.root}/app/models/members)) + # Sidekiq uses eager loading, but directories not in the standard Rails + # directories must be added to the eager load paths: + # https://github.com/mperham/sidekiq/wiki/FAQ#why-doesnt-sidekiq-autoload-my-rails-application-code + # Also, there is no need to add `lib` to autoload_paths since autoloading is + # configured to check for eager loaded paths: + # https://github.com/rails/rails/blob/v4.2.6/railties/lib/rails/engine.rb#L687 + # This is a nice reference article on autoloading/eager loading: + # http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload + config.eager_load_paths.push(*%W(#{config.root}/lib + #{config.root}/app/models/ci + #{config.root}/app/models/hooks + #{config.root}/app/models/members + #{config.root}/app/models/project_services)) + + config.generators.templates.push("#{config.root}/generator_templates") # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. @@ -32,7 +41,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - # + # # Parameters filtered: # - Password (:password, :password_confirmation) # - Private tokens (:private_token) @@ -71,6 +80,9 @@ module Gitlab config.assets.precompile << "*.png" config.assets.precompile << "print.css" config.assets.precompile << "notify.css" + config.assets.precompile << "mailers/*.css" + config.assets.precompile << "graphs/application.js" + config.assets.precompile << "users/application.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/boot.rb b/config/boot.rb index 4489e58688c..f2830ae3166 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -3,4 +3,4 @@ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/config/environments/development.rb b/config/environments/development.rb index 4f39016bfa4..8cca0039b4a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -39,6 +39,7 @@ Rails.application.configure do config.action_mailer.delivery_method = :letter_opener_web # Don't make a mess when bootstrapping a development environment config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') + config.action_mailer.preview_path = 'spec/mailers/previews' config.eager_load = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index a703c0934f7..fb25d3a8b14 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -20,7 +20,7 @@ Rails.application.configure do config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false + config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e682bcb976d..0510e7df597 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -98,6 +98,7 @@ production: &base wiki: true snippets: false builds: true + container_registry: true ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) @@ -175,6 +176,15 @@ production: &base repository_archive_cache_worker: cron: "0 * * * *" + registry: + # enabled: true + # host: registry.example.com + # port: 5005 + # api_url: http://localhost:5000/ # internal address to the registry, will be used by GitLab to directly communicate with API + # key_path: config/registry.key + # path: shared/registry + # issuer: gitlab-issuer + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8db2c05fe45..436751b9d16 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,4 +1,4 @@ -require 'gitlab' # Load lib/gitlab.rb as soon as possible +require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible class Settings < Settingslogic source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" } @@ -52,7 +52,7 @@ class Settings < Settingslogic # check that values in `current` (string or integer) is a contant in `modul`. def verify_constant_array(modul, current, default) values = default || [] - if !current.nil? + unless current.nil? values = [] current.each do |constant| values.push(verify_constant(modul, constant, nil)) @@ -126,7 +126,7 @@ end Settings['omniauth'] ||= Settingslogic.new({}) -Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? @@ -134,7 +134,7 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? -Settings.omniauth['providers'] ||= [] +Settings.omniauth['providers'] ||= [] Settings.omniauth['cas3'] ||= Settingslogic.new({}) Settings.omniauth.cas3['session_duration'] ||= 8.hours Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) @@ -168,7 +168,7 @@ end Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) -Settings['issues_tracker'] ||= {} +Settings['issues_tracker'] ||= {} # # GitLab @@ -183,7 +183,7 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' -Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" +Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' @@ -196,7 +196,7 @@ Settings.gitlab['user_home'] ||= begin rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end -Settings.gitlab['time_zone'] ||= nil +Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) @@ -206,12 +206,13 @@ Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['session_expire_delay'] ||= 10080 -Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? -Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? -Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? -Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? -Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? -Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) +Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? +Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? +Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? +Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? +Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] @@ -225,8 +226,8 @@ Settings['gitlab_ci'] ||= Settingslogic.new({}) Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil? Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? -Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) +Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) # # Reply by email @@ -240,7 +241,20 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'] Settings['artifacts'] ||= Settingslogic.new({}) Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) -Settings.artifacts['max_size'] ||= 100 # in megabytes +Settings.artifacts['max_size'] ||= 100 # in megabytes + +# +# Registry +# +Settings['registry'] ||= Settingslogic.new({}) +Settings.registry['enabled'] ||= false +Settings.registry['host'] ||= "example.com" +Settings.registry['port'] ||= nil +Settings.registry['api_url'] ||= "http://localhost:5000/" +Settings.registry['key'] ||= nil +Settings.registry['issuer'] ||= nil +Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') +Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root) # # Git LFS @@ -298,7 +312,7 @@ Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['pg_schema'] = nil Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) -Settings.backup['archive_permissions'] ||= 0600 +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 80d641d73a3..e026151a032 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -1,11 +1,11 @@ # GIT over HTTP -require Rails.root.join("lib", "gitlab", "backend", "grack_auth") +require_dependency Rails.root.join('lib/gitlab/backend/grack_auth') # GIT over SSH -require Rails.root.join("lib", "gitlab", "backend", "shell") +require_dependency Rails.root.join('lib/gitlab/backend/shell') # GitLab shell adapter -require Rails.root.join("lib", "gitlab", "backend", "shell_adapter") +require_dependency Rails.root.join('lib/gitlab/backend/shell_adapter') required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index df28d30d750..1933afcbfb1 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -2,7 +2,7 @@ CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ aws_file = Rails.root.join('config', 'aws.yml') -if File.exists?(aws_file) +if File.exist?(aws_file) AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env] CarrierWave.configure do |config| @@ -20,7 +20,7 @@ if File.exists?(aws_file) config.fog_public = false # optional, defaults to {} - config.fog_attributes = { 'Cache-Control'=>'max-age=315576000' } + config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' } # optional time (in seconds) that authenticated urls will be valid. # when fog_public is false and provider is AWS or Google, defaults to 600 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 31dceaebcad..021bdb11251 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -243,7 +243,7 @@ Devise.setup do |config| when Hash # Add procs for handling SLO if provider['name'] == 'cas3' - provider['args'][:on_single_sign_out] = lambda do |request| + provider['args'][:on_single_sign_out] = lambda do |request| ticket = request.params[:session_index] raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) Gitlab::OAuth::Session.destroy(:cas3, ticket) diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb deleted file mode 100644 index 05a1852cdbd..00000000000 --- a/config/initializers/devise_async.rb +++ /dev/null @@ -1 +0,0 @@ -Devise::Async.backend = :sidekiq diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 66ac88e9f4a..7bd13105045 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -52,7 +52,7 @@ Doorkeeper.configure do # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :api - #optional_scopes :write, :update + # optional_scopes :write, :update # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then @@ -71,7 +71,7 @@ Doorkeeper.configure do # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) # - native_redirect_uri nil#'urn:ietf:wg:oauth:2.0:oob' + native_redirect_uri nil # 'urn:ietf:wg:oauth:2.0:oob' # Specify what grant flows are enabled in array of Strings. The valid # strings and the flows they enable are: diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb new file mode 100644 index 00000000000..79e2d23ab2e --- /dev/null +++ b/config/initializers/health_check.rb @@ -0,0 +1,3 @@ +HealthCheck.setup do |config| + config.standard_checks = ['database', 'migrations', 'cache'] +end diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index b2d08d87bac..0c788714714 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -12,6 +12,7 @@ if Gitlab::Metrics.enabled? Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) + config.middleware.use(Gitlab::Middleware::RailsQueueDuration) end Sidekiq.configure_server do |config| @@ -118,6 +119,8 @@ if Gitlab::Metrics.enabled? # Instrument the classes used for checking if somebody has push access. config.instrument_instance_methods(Gitlab::GitAccess) config.instrument_instance_methods(Gitlab::GitAccessWiki) + + config.instrument_instance_methods(API::Helpers) end GC::Profiler.enable diff --git a/config/initializers/monkey_patch.rb b/config/initializers/monkey_patch.rb deleted file mode 100644 index 62b05a55285..00000000000 --- a/config/initializers/monkey_patch.rb +++ /dev/null @@ -1,48 +0,0 @@ -## This patch is from rails 4.2-stable. Remove it when 4.2.6 is released -## https://github.com/rails/rails/issues/21108 - -module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter < AbstractAdapter - # SHOW VARIABLES LIKE 'name' - def show_variable(name) - variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') - variables.first['Value'] unless variables.empty? - rescue ActiveRecord::StatementInvalid - nil - end - - - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') - end - end - end -end - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "select @@character_set_client", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - end - end -end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 4c164119fff..26c30e523a7 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -13,7 +13,7 @@ end OmniAuth.config.full_host = Settings.gitlab['base_url'] OmniAuth.config.allowed_request_methods = [:post] -#In case of auto sign-in, the GET method is used (users don't get to click on a button) +# In case of auto sign-in, the GET method is used (users don't get to click on a button) OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? OmniAuth.config.before_request_phase do |env| OmniAuth::RequestForgeryProtection.call(env) diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index b9176688bc4..cb00d3cfe95 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -3,6 +3,6 @@ Premailer::Rails.config.merge!( generate_text_part: false, preserve_styles: true, remove_comments: true, - remove_ids: true, + remove_ids: false, remove_scripts: false ) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 599dabb9e50..0d9d87bac00 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -23,6 +23,6 @@ else secure: Gitlab.config.gitlab.https, httponly: true, expires_in: Settings.gitlab['session_expire_delay'] * 60, - path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root + path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root ) end diff --git a/config/routes.rb b/config/routes.rb index dafecc94648..428302d0fd7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,7 @@ Rails.application.routes.draw do # Autocomplete get '/autocomplete/users' => 'autocomplete#users' get '/autocomplete/users/:id' => 'autocomplete#user' + get '/autocomplete/projects' => 'autocomplete#projects' # Emojis resources :emojis, only: :index @@ -64,6 +65,9 @@ Rails.application.routes.draw do get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete + # JSON Web Token + get 'jwt/auth' => 'jwt#auth' + # API API::API.logger Rails.logger mount API::API => '/api' @@ -73,6 +77,9 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end + # Health check + get 'health_check(/:checks)' => 'health_check#index', as: :health_check + # Enable Grack support mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] @@ -80,7 +87,7 @@ Rails.application.routes.draw do get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } get 'help/shortcuts' - get 'help/ui' => 'help#ui' + get 'help/ui' => 'help#ui' # # Global snippets @@ -91,7 +98,8 @@ Rails.application.routes.draw do end end - get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } + get '/s/:username', to: redirect('/u/%{username}/snippets'), + constraints: { username: /[a-zA-Z.0-9_\-]+(? 'users#calendar', as: :user_calendar, - constraints: { username: /.*/ } - - get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, - constraints: { username: /.*/ } - - get 'u/:username/groups' => 'users#groups', as: :user_groups, - constraints: { username: /.*/ } - - get 'u/:username/projects' => 'users#projects', as: :user_projects, - constraints: { username: /.*/ } - - get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, - constraints: { username: /.*/ } - - get '/u/:username' => 'users#show', as: :user, - constraints: { username: /[a-zA-Z.0-9_\-]+(? 'omniauth_callbacks#omniauth_error', as: :omniauth_error @@ -659,9 +668,16 @@ Rails.application.routes.draw do end resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resource :variables, only: [:show, :update] + resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :destroy] + resources :pipelines, only: [:index, :new, :create, :show] do + member do + post :cancel + post :retry + end + end + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all @@ -672,6 +688,7 @@ Rails.application.routes.draw do post :cancel post :retry post :erase + get :trace get :raw end @@ -688,6 +705,8 @@ Rails.application.routes.draw do end end + resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex } + resources :milestones, constraints: { id: /\d+/ } do member do put :sort_issues @@ -774,7 +793,7 @@ Rails.application.routes.draw do end # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } + get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ } get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } end diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb index 8a351cf27a3..6aed0fe03d2 100644 --- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb +++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb @@ -24,11 +24,11 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration def process_projects_with_wrong_url projects_with_wrong_import_url.each do |project| begin - import_url = Gitlab::ImportUrl.new(project["import_url"]) + import_url = Gitlab::UrlSanitizer.new(project["import_url"]) update_import_url(import_url, project) update_import_data(import_url, project) - rescue URI::InvalidURIError + rescue Addressable::URI::InvalidURIError nullify_import_url(project) end end diff --git a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb index 75de5f70fa2..72b862d67d2 100644 --- a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb +++ b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb @@ -7,7 +7,9 @@ class AddDefaultGroupVisibilityToApplicationSettings < ActiveRecord::Migration add_column :application_settings, :default_group_visibility, :integer # Unfortunately, this can't be a `default`, since we don't want the configuration specific # `allowed_visibility_level` to end up in schema.rb - execute("UPDATE application_settings SET default_group_visibility = #{allowed_visibility_level}") + + visibility_level = allowed_visibility_level || Gitlab::VisibilityLevel::PRIVATE + execute("UPDATE application_settings SET default_group_visibility = #{visibility_level}") end def down diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb new file mode 100644 index 00000000000..47f0ca8e8de --- /dev/null +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -0,0 +1,5 @@ +class AddImagesEnabledForProject < ActiveRecord::Migration + def change + add_column :projects, :container_registry_enabled, :boolean + end +end diff --git a/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb new file mode 100644 index 00000000000..facd33875ba --- /dev/null +++ b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddDisabledOauthSignInSourcesToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :disabled_oauth_sign_in_sources, :text + end +end diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb new file mode 100644 index 00000000000..84e5e4eabe2 --- /dev/null +++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb @@ -0,0 +1,13 @@ +class AddRunUntaggedToCiRunner < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:ci_runners, :run_untagged, :boolean, + default: true, allow_null: false) + end + + def down + remove_column(:ci_runners, :run_untagged) + end +end diff --git a/db/migrate/20160508215820_add_type_to_notes.rb b/db/migrate/20160508215820_add_type_to_notes.rb new file mode 100644 index 00000000000..58944d4e651 --- /dev/null +++ b/db/migrate/20160508215820_add_type_to_notes.rb @@ -0,0 +1,5 @@ +class AddTypeToNotes < ActiveRecord::Migration + def change + add_column :notes, :type, :string + end +end diff --git a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb new file mode 100644 index 00000000000..c3f23d89d5a --- /dev/null +++ b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb @@ -0,0 +1,5 @@ +class SetTypeOnLegacyDiffNotes < ActiveRecord::Migration + def change + execute "UPDATE notes SET type = 'LegacyDiffNote' WHERE line_code IS NOT NULL" + end +end diff --git a/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb new file mode 100644 index 00000000000..9d729fec189 --- /dev/null +++ b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddHealthCheckAccessTokenToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :health_check_access_token, :string + end +end diff --git a/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb new file mode 100644 index 00000000000..c34e7ba5409 --- /dev/null +++ b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb @@ -0,0 +1,12 @@ +class AddSendUserConfirmationEmailToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :send_user_confirmation_email, :boolean, default: false + + #Sets confirmation email to true by default on existing installations. + execute "UPDATE application_settings SET send_user_confirmation_email=true" + end + + def down + remove_column :application_settings, :send_user_confirmation_email + end +end diff --git a/db/migrate/20160525205328_remove_main_language_from_projects.rb b/db/migrate/20160525205328_remove_main_language_from_projects.rb new file mode 100644 index 00000000000..0f9d60c385f --- /dev/null +++ b/db/migrate/20160525205328_remove_main_language_from_projects.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveMainLanguageFromProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # 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 + remove_column :projects, :main_language + end +end diff --git a/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb new file mode 100644 index 00000000000..7910120b4e0 --- /dev/null +++ b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb @@ -0,0 +1,13 @@ +class RemoveNotificationSettingsForDeletedProjects < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM notification_settings + WHERE notification_settings.source_type = 'Project' + AND NOT EXISTS ( + SELECT * + FROM projects + WHERE projects.id = notification_settings.source_id + ) + SQL + end +end diff --git a/db/migrate/20160528043124_add_users_state_index.rb b/db/migrate/20160528043124_add_users_state_index.rb new file mode 100644 index 00000000000..e77a5460737 --- /dev/null +++ b/db/migrate/20160528043124_add_users_state_index.rb @@ -0,0 +1,9 @@ +class AddUsersStateIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :users, :state + end +end diff --git a/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb new file mode 100644 index 00000000000..e21376bd571 --- /dev/null +++ b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb @@ -0,0 +1,9 @@ +# This is ONLINE migration + +class AddContainerRegistryTokenExpireDelayToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column :application_settings, :container_registry_token_expire_delay, :integer, default: 5 + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d953afe30..b2af810f600 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160508194200) do +ActiveRecord::Schema.define(version: 20160530150109) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -43,43 +43,47 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.datetime "created_at" t.datetime "updated_at" t.string "home_page_url" - t.integer "default_branch_protection", default: 2 + t.integer "default_branch_protection", default: 2 t.text "restricted_visibility_levels" - t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", default: 10, null: false + t.boolean "version_check_enabled", default: true + t.integer "max_attachment_size", default: 10, null: false t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" - t.boolean "user_oauth_applications", default: true + t.boolean "user_oauth_applications", default: true t.string "after_sign_out_path" - t.integer "session_expire_delay", default: 10080, null: false + t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" t.text "help_page_text" t.string "admin_notification_email" - t.boolean "shared_runners_enabled", default: true, null: false - t.integer "max_artifacts_size", default: 100, null: false + t.boolean "shared_runners_enabled", default: true, null: false + t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" - t.boolean "require_two_factor_authentication", default: false - t.integer "two_factor_grace_period", default: 48 - t.boolean "metrics_enabled", default: false - t.string "metrics_host", default: "localhost" - t.integer "metrics_pool_size", default: 16 - t.integer "metrics_timeout", default: 10 - t.integer "metrics_method_call_threshold", default: 10 - t.boolean "recaptcha_enabled", default: false + t.boolean "require_two_factor_authentication", default: false + t.integer "two_factor_grace_period", default: 48 + t.boolean "metrics_enabled", default: false + t.string "metrics_host", default: "localhost" + t.integer "metrics_pool_size", default: 16 + t.integer "metrics_timeout", default: 10 + t.integer "metrics_method_call_threshold", default: 10 + t.boolean "recaptcha_enabled", default: false t.string "recaptcha_site_key" t.string "recaptcha_private_key" - t.integer "metrics_port", default: 8089 - t.boolean "akismet_enabled", default: false + t.integer "metrics_port", default: 8089 + t.boolean "akismet_enabled", default: false t.string "akismet_api_key" - t.integer "metrics_sample_interval", default: 15 - t.boolean "sentry_enabled", default: false + t.integer "metrics_sample_interval", default: 15 + t.boolean "sentry_enabled", default: false t.string "sentry_dsn" - t.boolean "email_author_in_body", default: false + t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" - t.boolean "repository_checks_enabled", default: false + t.boolean "repository_checks_enabled", default: false t.text "shared_runners_text" - t.integer "metrics_packet_size", default: 1 + t.integer "metrics_packet_size", default: 1 + t.text "disabled_oauth_sign_in_sources" + t.string "health_check_access_token" + t.boolean "send_user_confirmation_email", default: false + t.integer "container_registry_token_expire_delay", default: 5 end create_table "audit_events", force: :cascade do |t| @@ -267,6 +271,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "revision" t.string "platform" t.string "architecture" + t.boolean "run_untagged", default: true, null: false end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} @@ -634,6 +639,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.text "st_diff" t.integer "updated_by_id" t.boolean "is_award", default: false, null: false + t.string "type" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -755,10 +761,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false t.boolean "public_builds", default: true, null: false - t.string "main_language" t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" + t.boolean "container_registry_enabled" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -995,6 +1001,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + add_index "users", ["state"], name: "index_users_on_state", using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"} diff --git a/doc/README.md b/doc/README.md index e358da1c424..d1345ab2493 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,6 +13,7 @@ - [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. +- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. @@ -41,8 +42,10 @@ - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. - [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs - [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 diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md new file mode 100644 index 00000000000..caf9a5bef2c --- /dev/null +++ b/doc/administration/container_registry.md @@ -0,0 +1,375 @@ +# GitLab Container Registry Administration + +> **Note:** +This feature was [introduced][ce-4040] in GitLab 8.8. + +With the Docker Container Registry integrated into GitLab, every project can +have its own space to store its Docker images. + +You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. + +--- + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Enable the Container Registry](#enable-the-container-registry) +- [Container Registry domain configuration](#container-registry-domain-configuration) + - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain) + - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain) +- [Disable Container Registry site-wide](#disable-container-registry-site-wide) +- [Disable Container Registry per project](#disable-container-registry-per-project) +- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide) +- [Container Registry storage path](#container-registry-storage-path) +- [Storage limitations](#storage-limitations) +- [Changelog](#changelog) + + + +## Enable the Container Registry + +**Omnibus GitLab installations** + +All you have to do is configure the domain name under which the Container +Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration) +and pick one of the two options that fits your case. + +>**Note:** +The container Registry works under HTTPS by default. Using HTTP is possible +but not recommended and out of the scope of this document. +Read the [insecure Registry documentation][docker-insecure] if you want to +implement this. + +--- + +**Installations from source** + +If you have installed GitLab from source: + +1. You will have to [install Docker Registry][registry-deploy] by yourself. +1. After the installation is complete, you will have to configure the Registry's + settings in `gitlab.yml` in order to enable it. +1. Use the sample NGINX configuration file that is found under + [`lib/support/nginx/registry-ssl`][registry-ssl] and edit it to match the + `host`, `port` and TLS certs paths. + +The contents of `gitlab.yml` are: + +``` +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key_path: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` + +where: + +| Parameter | Description | +| --------- | ----------- | +| `enabled` | `true` or `false`. Enables the Registry in GitLab. By default this is `false`. | +| `host` | The host URL under which the Registry will run and the users will be able to use. | +| `port` | The port under which the external Registry domain will listen on. | +| `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. | +| `key_path`| The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. | +| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#container-registry-storage-path](#container-registry-storage-path). | +| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. | + +>**Note:** +GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab] +will not restart the Registry should you modify its settings. Read the upstream +documentation on how to achieve that. + +## Container Registry domain configuration + +There are two ways you can configure the Registry's external domain. + +- Either [use the existing GitLab domain][existing-domain] where in that case + the Registry will have to listen on a port and reuse GitLab's TLS certificate, +- or [use a completely separate domain][new-domain] with a new TLS certificate + for that domain. + +Since the container Registry requires a TLS certificate, in the end it all boils +down to how easy or pricey is to get a new one. + +Please take this into consideration before configuring the Container Registry +for the first time. + +### Configure Container Registry under an existing GitLab domain + +If the Registry is configured to use the existing GitLab domain, you can +expose the Registry on a port so that you can reuse the existing GitLab TLS +certificate. + +Assuming that the GitLab domain is `https://gitlab.example.com` and the port the +Registry is exposed to the outside world is `4567`, here is what you need to set +in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed +GitLab from source respectively. + +--- + +**Omnibus GitLab installations** + +1. Your `/etc/gitlab/gitlab.rb` should contain the Registry URL as well as the + path to the existing TLS certificate and key used by GitLab: + + ```ruby + registry_external_url 'https://gitlab.example.com:4567' + ``` + + Note how the `registry_external_url` is listening on HTTPS under the + existing GitLab URL, but on a different port. + + If your TLS certificate is not in `/etc/gitlab/ssl/gitlab.example.com.crt` + and key not in `/etc/gitlab/ssl/gitlab.example.com.key` uncomment the lines + below: + + ```ruby + registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" + registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + configure it with the following settings: + + ``` + registry: + enabled: true + host: gitlab.example.com + port: 4567 + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. +1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path). + +--- + +Users should now be able to login to the Container Registry with their GitLab +credentials using: + +```bash +docker login gitlab.example.com:4567 +``` + +### Configure Container Registry under its own domain + +If the Registry is configured to use its own domain, you will need a TLS +certificate for that specific domain (e.g., `registry.example.com`) or maybe +a wildcard certificate if hosted under a subdomain of your existing GitLab +domain (e.g., `registry.gitlab.example.com`). + +Let's assume that you want the container Registry to be accessible at +`https://registry.gitlab.example.com`. + +--- + +**Omnibus GitLab installations** + +1. Place your TLS certificate and key in + `/etc/gitlab/ssl/registry.gitlab.example.com.crt` and + `/etc/gitlab/ssl/registry.gitlab.example.com.key` and make sure they have + correct permissions: + + ```bash + chmod 600 /etc/gitlab/ssl/registry.gitlab.example.com.* + ``` + +1. Once the TLS certificate is in place, edit `/etc/gitlab/gitlab.rb` with: + + ```ruby + registry_external_url 'https://registry.gitlab.example.com' + ``` + + Note how the `registry_external_url` is listening on HTTPS. + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +> **Note:** +If you have a [wildcard certificate][], you need to specify the path to the +certificate in addition to the URL, in this case `/etc/gitlab/gitlab.rb` will +look like: +> +```ruby +registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" +registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" +``` + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + configure it with the following settings: + + ``` + registry: + enabled: true + host: registry.gitlab.example.com + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. +1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path). + +--- + +Users should now be able to login to the Container Registry using their GitLab +credentials: + +```bash +docker login registry.gitlab.example.com +``` + +## Disable Container Registry site-wide + +>**Note:** +Disabling the Registry in the Rails GitLab application as set by the following +steps, will not remove any existing Docker images. This is handled by the +Registry application itself. + +**Omnibus GitLab** + +1. Open `/etc/gitlab/gitlab.rb` and set `registry['enable']` to `false`: + + ```ruby + registry['enable'] = false + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + set `enabled` to `false`: + + ``` + registry: + enabled: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Disable Container Registry per project + +If Registry is enabled in your GitLab instance, but you don't need it for your +project, you can disable it from your project's settings. Read the user guide +on how to achieve that. + +## Disable Container Registry for new projects site-wide + +If the Container Registry is enabled, then it will be available on all new +projects. To disable this function and let the owners of a project to enable +the Container Registry by themselves, follow the steps below. + +--- + +**Omnibus GitLab installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['gitlab_default_projects_features_container_registry'] = false + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `default_projects_features` + entry and configure it so that `container_registry` is set to `false`: + + ``` + ## Default project features settings + default_projects_features: + issues: true + merge_requests: true + wiki: true + snippets: false + builds: true + container_registry: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Container Registry storage path + +To change the storage path where Docker images will be stored, follow the +steps below. + +This path is accessible to: + +- the user running the Container Registry daemon, +- the user running GitLab + +> **Warning** You should confirm that all GitLab, Registry and web server users +have access to this directory. + +--- + +**Omnibus GitLab installations** + +The default location where images are stored in Omnibus, is +`/var/opt/gitlab/gitlab-rails/shared/registry`. To change it: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['registry_path'] = "/path/to/registry/storage" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +The default location where images are stored in source installations, is +`/home/git/gitlab/shared/registry`. To change it: + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + change the `path` setting: + + ``` + registry: + path: shared/registry + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Storage limitations + +Currently, there is no storage limitation, which means a user can upload an +infinite amount of Docker images with arbitrary sizes. This setting will be +configurable in future releases. + +## Changelog + +**GitLab 8.8 ([source docs][8-8-docs])** + +- GitLab Container Registry feature was introduced. + +[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 +[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[docker-insecure]: https://docs.docker.com/registry/insecure/ +[registry-deploy]: https://docs.docker.com/registry/deploying/ +[storage-config]: https://docs.docker.com/registry/configuration/#storage +[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 +[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain +[new-domain]: #configure-container-registry-under-its-own-domain diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 43ab153d76d..7f53915a4d7 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -58,4 +58,4 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. It's possible to preconfigure the GitLab docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. -For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container). +For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container). diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 43d85ffb775..d74a786ac24 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -19,6 +19,8 @@ Components/Servers Required: - 2 servers/virtual machines (one active/one passive) +![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png) + ### Active/Active This architecture scales easily because all application servers handle @@ -26,6 +28,8 @@ user requests simultaneously. The database, Redis, and GitLab application are all deployed on separate servers. The configuration is **only** highly-available if the database, Redis and storage are also configured as such. +![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png) + **Steps to configure active/active:** 1. [Configure the database](database.md) diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index b1fe34ed9a1..136f570ac27 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -60,4 +60,4 @@ Read more on high-availability configuration: configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. -[gitlab-pages]: http://doc.gitlab.com/ee/pages/administration.html +[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index e4e124e200a..537f4f3501d 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -2,8 +2,8 @@ ## Required NFS Server features -**File locking**: GitLab **requires** file locking which is only supported -natively in NFS version 4. NFSv3 also supports locking as long as +**File locking**: GitLab **requires** advisory file locking, which is only +supported natively in NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not specifically test NFSv3. @@ -113,4 +113,4 @@ Read more on high-availability configuration: 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) -[udp-log-shipping]: http://doc.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" +[udp-log-shipping]: http://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index d89a1e582ca..f6153216f33 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -26,7 +26,7 @@ that runs Redis. ```ruby external_url 'https://gitlab.example.com' - # Disable all components except PostgreSQL + # Disable all components except Redis redis['enable'] = true bootstrap['enable'] = false nginx['enable'] = false diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png new file mode 100644 index 00000000000..81259e0ae93 Binary files /dev/null and b/doc/administration/img/high_availability/active-active-diagram.png differ diff --git a/doc/administration/img/high_availability/active-passive-diagram.png b/doc/administration/img/high_availability/active-passive-diagram.png new file mode 100644 index 00000000000..f69ff1d0357 Binary files /dev/null and b/doc/administration/img/high_availability/active-passive-diagram.png differ diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 3411e4af6a7..4172b604cec 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -5,7 +5,7 @@ This feature was [introduced][ce-3232] in GitLab 8.7. It is OFF by default because it still causes too many false alarms. Git has a built-in mechanism, [git fsck][git-fsck], to verify the -integrity of all data commited to a repository. GitLab administrators +integrity of all data committed to a repository. GitLab administrators can trigger such a check for a project via the project page under the admin panel. The checks run asynchronously so it may take a few minutes before the check result is visible on the project admin page. If the @@ -41,4 +41,4 @@ alarms you can choose to clear ALL repository check states from the --- [ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck" -[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" \ No newline at end of file +[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index 134a7583762..a776cd3f05e 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -150,6 +150,14 @@ To output a backtrace from all threads at once: apply all thread bt ``` +Once you're done debugging with `gdb`, be sure to detach from the process and +exit: + +``` +detach +exit +``` + ## Check for blocking queries Sometimes the speed at which Sidekiq processes jobs can be so fast that it can diff --git a/doc/api/README.md b/doc/api/README.md index ff039f1886f..27c5962decf 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,7 +33,7 @@ following locations: - [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Runners](runners.md) -- [Licenses](licenses.md) +- [Open source license templates](licenses.md) ## Authentication diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index 4a12e962b62..0881a7d7a90 100644 --- a/doc/api/build_triggers.md +++ b/doc/api/build_triggers.md @@ -101,8 +101,18 @@ DELETE /projects/:id/triggers/:token | Attribute | Type | required | Description | |-----------|---------|----------|--------------------------| | `id` | integer | yes | The ID of a project | -| `token` | string | yes | The `token` of a project | +| `token` | string | yes | The `token` of a trigger | ``` curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` + +```json +{ + "created_at": "2015-12-23T16:25:56.760Z", + "deleted_at": "2015-12-24T12:32:20.100Z", + "last_used": null, + "token": "7b9148c158980bbd9bcea92c17522d", + "updated_at": "2015-12-24T12:32:20.100Z" +} +``` diff --git a/doc/api/groups.md b/doc/api/groups.md index 2821bc21b81..1ccb9715e96 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -265,7 +265,6 @@ GET /groups/:id/members { "id": 1, "username": "raymond_smith", - "email": "ray@smith.org", "name": "Raymond Smith", "state": "active", "created_at": "2012-10-22T14:13:35Z", @@ -274,7 +273,6 @@ GET /groups/:id/members { "id": 2, "username": "john_doe", - "email": "joh@doe.org", "name": "John Doe", "state": "active", "created_at": "2012-10-22T14:13:35Z", diff --git a/doc/api/issues.md b/doc/api/issues.md index 3e78149f442..fc7a7ae0c0c 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -77,7 +77,8 @@ Example response: "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, "labels" : [], - "subscribed" : false + "subscribed" : false, + "user_notes_count": 1 } ] ``` @@ -154,7 +155,8 @@ Example response: "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", - "subscribed" : false + "subscribed" : false, + "user_notes_count": 1 } ] ``` @@ -216,7 +218,8 @@ Example response: "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", - "subscribed": false + "subscribed": false, + "user_notes_count": 1 } ``` @@ -271,7 +274,8 @@ Example response: "description" : null, "updated_at" : "2016-01-07T12:44:33.959Z", "milestone" : null, - "subscribed" : true + "subscribed" : true, + "user_notes_count": 0 } ``` @@ -329,7 +333,8 @@ Example response: "id" : 85, "assignee" : null, "milestone" : null, - "subscribed" : true + "subscribed" : true, + "user_notes_count": 0 } ``` diff --git a/doc/api/labels.md b/doc/api/labels.md index 3730c07c5a7..a181c0f57a2 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -39,7 +39,7 @@ Example response: { "name" : "critical", "color" : "#d9534f", - "description": "Criticalissue. Need fix ASAP", + "description": "Critical issue. Need fix ASAP", "open_issues_count": 1, "closed_issues_count": 3, "open_merge_requests_count": 1 @@ -165,3 +165,73 @@ Example response: "description": "Documentation" } ``` + +## Subscribe to a label + +Subscribes the authenticated user to a label to receive notifications. If the +operation is successful, status code `201` together with the updated label is +returned. If the user is already subscribed to the label, the status code `304` +is returned. If the project or label is not found, status code `404` is +returned. + +``` +POST /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": true +} +``` + +## Unsubscribe from a label + +Unsubscribes the authenticated user from a label to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated label is returned. If the user is not subscribed to the label, the +status code `304` is returned. If the project or label is not found, status code +`404` is returned. + +``` +DELETE /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 2057f9d77aa..8217e30fe25 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -67,7 +67,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : false + "subscribed" : false, + "user_notes_count": 1 } ] ``` @@ -130,7 +131,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -230,6 +232,7 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "user_notes_count": 1, "changes": [ { "old_path": "VERSION", @@ -308,7 +311,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 0 } ``` @@ -378,7 +382,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -472,7 +477,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -537,7 +543,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -602,7 +609,8 @@ Example response: "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, - "labels" : [] + "labels" : [], + "user_notes_count": 1 }, ] ``` diff --git a/doc/api/notes.md b/doc/api/notes.md index a6b5b1787fd..7aa1c2155bf 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -15,7 +15,7 @@ GET /projects/:id/issues/:issue_id/notes Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of an issue (not ID) +- `issue_id` (required) - The ID of an issue ```json [ @@ -73,7 +73,7 @@ GET /projects/:id/issues/:issue_id/notes/:note_id Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of a project issue (not ID) +- `issue_id` (required) - The ID of a project issue - `note_id` (required) - The ID of an issue note ### Create new issue note @@ -87,7 +87,7 @@ POST /projects/:id/issues/:issue_id/notes Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of an issue (not ID) +- `issue_id` (required) - The ID of an issue - `body` (required) - The content of a note - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z @@ -102,7 +102,7 @@ PUT /projects/:id/issues/:issue_id/notes/:note_id Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of an issue (not ID) +- `issue_id` (required) - The ID of an issue - `note_id` (required) - The ID of a note - `body` (required) - The content of a note @@ -120,7 +120,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The IID of an issue | +| `issue_id` | integer | yes | The ID of an issue | | `note_id` | integer | yes | The ID of a note | ```bash diff --git a/doc/api/projects.md b/doc/api/projects.md index de1faadebf5..f5f195b97df 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -424,6 +424,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -447,6 +448,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -472,6 +474,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) diff --git a/doc/api/runners.md b/doc/api/runners.md index cc6c6b7cb2f..ddfa298f79d 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -275,7 +275,7 @@ POST /projects/:id/runners | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners" -F "runner_id=9" +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9" ``` Example response: @@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners/9" +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" ``` Example response: diff --git a/doc/api/services.md b/doc/api/services.md index 7d45b2cf463..ccfc0fccb7f 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -16,8 +16,8 @@ PUT /projects/:id/services/asana Parameters: -- `api_key` (**required**) - User API token. User must have access to task,all comments will be attributed to this user. -- `restrict_to_branch` (optional) - Comma-separated list of branches which will beautomatically inspected. Leave blank to include all branches. +- `api_key` (**required**) - User API token. User must have access to task, all comments will be attributed to this user. +- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches. ### Delete Asana service @@ -491,7 +491,7 @@ Jira issue tracker Set JIRA service for a project. -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html) +> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) ``` PUT /projects/:id/services/jira @@ -503,6 +503,8 @@ Parameters: - `project_url` (**required**) - Project url - `issues_url` (**required**) - Issue url - `description` (optional) - Jira issue tracker +- `username` (optional) - Jira username +- `password` (optional) - Jira password ### Delete JIRA service diff --git a/doc/api/settings.md b/doc/api/settings.md index 1e745115dc8..43a0fe35e42 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -37,7 +37,8 @@ Example response: "created_at" : "2016-01-04T15:44:55.176Z", "default_project_visibility" : 0, "gravatar_enabled" : true, - "sign_in_text" : null + "sign_in_text" : null, + "container_registry_token_expire_delay": 5 } ``` @@ -64,6 +65,7 @@ PUT /application/settings | `restricted_signup_domains` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | | `after_sign_out_path` | string | no | Where to redirect users after logout | +| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -90,6 +92,7 @@ Example response: "default_snippet_visibility": 0, "restricted_signup_domains": [], "user_oauth_applications": true, - "after_sign_out_path": "" + "after_sign_out_path": "", + "container_registry_token_expire_delay": 5 } ``` diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md deleted file mode 100644 index 7d91ce6710f..00000000000 --- a/doc/ci/deployment/README.md +++ /dev/null @@ -1,98 +0,0 @@ -## Using Dpl as deployment tool -Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI. - -**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**. - -### Requirements -To use Dpl you need at least Ruby 1.8.7 with ability to install gems. - -### Basic usage -The Dpl can be installed on any machine with: -``` -gem install dpl -``` - -This allows you to test all commands from your shell, rather than having to test it on a CI server. - -If you don't have Ruby installed you can do it on Debian-compatible Linux with: -``` -apt-get update -apt-get install ruby-dev -``` - -The Dpl provides support for vast number of services, including: Heroku, Cloud Foundry, AWS/S3, and more. -To use it simply define provider and any additional parameters required by the provider. - -For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`. -There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku - -``` -staging: - type: deploy - - gem install dpl - - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY -``` - -In the above example we use Dpl to deploy `my-app-staging` to Heroku server with api-key stored in `HEROKU_STAGING_API_KEY` secure variable. - -To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers). - -### Using Dpl with Docker -When you use GitLab Runner you most likely configured it to use your server's shell commands. -This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner). -It also means that most probably in your Docker container you don't have the Ruby runtime installed. -You will have to install it: -``` -staging: - type: deploy - - apt-get update -yq - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY - only: - - master -``` - -The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system. -The above example is valid for all Debian-compatible systems. - -### Usage in staging and production -It's pretty common in developer workflow to have staging (development) and production environment. -If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment. -The final `.gitlab-ci.yml` for that setup would look like this: - -``` -staging: - type: deploy - - gem install dpl - - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY - only: - - master - -production: - type: deploy - - gem install dpl - - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY - only: - - tags -``` - -We created two deploy jobs that are executed on different events: -1. `staging` is executed for all commits that were pushed to `master` branch, -2. `production` is executed for all pushed tags. - -We also use two secure variables: -1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, -2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. - -### Storing API keys -In GitLab CI 7.12 a new feature was introduced: Secure Variables. -Secure Variables can added by going to `Project > Variables > Add Variable`. -**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.** -The variables that are defined in the project settings are sent along with the build script to the runner. -The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml. -It is also important that secret's value is hidden in the build log. - -You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners): -1. `$SECRET_VARIABLE` - use it for non-Windows runners -2. `%SECRET_VARIABLE%` - use it for Windows Batch runners diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md new file mode 100644 index 00000000000..7d91ce6710f --- /dev/null +++ b/doc/ci/examples/deployment/README.md @@ -0,0 +1,98 @@ +## Using Dpl as deployment tool +Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI. + +**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**. + +### Requirements +To use Dpl you need at least Ruby 1.8.7 with ability to install gems. + +### Basic usage +The Dpl can be installed on any machine with: +``` +gem install dpl +``` + +This allows you to test all commands from your shell, rather than having to test it on a CI server. + +If you don't have Ruby installed you can do it on Debian-compatible Linux with: +``` +apt-get update +apt-get install ruby-dev +``` + +The Dpl provides support for vast number of services, including: Heroku, Cloud Foundry, AWS/S3, and more. +To use it simply define provider and any additional parameters required by the provider. + +For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`. +There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku + +``` +staging: + type: deploy + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY +``` + +In the above example we use Dpl to deploy `my-app-staging` to Heroku server with api-key stored in `HEROKU_STAGING_API_KEY` secure variable. + +To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers). + +### Using Dpl with Docker +When you use GitLab Runner you most likely configured it to use your server's shell commands. +This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner). +It also means that most probably in your Docker container you don't have the Ruby runtime installed. +You will have to install it: +``` +staging: + type: deploy + - apt-get update -yq + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master +``` + +The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system. +The above example is valid for all Debian-compatible systems. + +### Usage in staging and production +It's pretty common in developer workflow to have staging (development) and production environment. +If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment. +The final `.gitlab-ci.yml` for that setup would look like this: + +``` +staging: + type: deploy + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master + +production: + type: deploy + - gem install dpl + - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY + only: + - tags +``` + +We created two deploy jobs that are executed on different events: +1. `staging` is executed for all commits that were pushed to `master` branch, +2. `production` is executed for all pushed tags. + +We also use two secure variables: +1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, +2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. + +### Storing API keys +In GitLab CI 7.12 a new feature was introduced: Secure Variables. +Secure Variables can added by going to `Project > Variables > Add Variable`. +**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.** +The variables that are defined in the project settings are sent along with the build script to the runner. +The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml. +It is also important that secret's value is hidden in the build log. + +You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners): +1. `$SECRET_VARIABLE` - use it for non-Windows runners +2. `%SECRET_VARIABLE%` - use it for Windows Batch runners diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 6a42a935abd..386b8e29fcf 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the builds, you should explicitly enable the **Builds Emails** service under your project's settings. -For more information read the [Builds emails service documentation] -(../../project_services/builds_emails.md). +For more information read the +[Builds emails service documentation](../../project_services/builds_emails.md). ## Builds badge diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index a06650b3387..b42d7a62ebc 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -125,7 +125,13 @@ shared runners will only run the jobs they are equipped to run. For instance, at GitLab we have runners tagged with "rails" if they contain the appropriate dependencies to run Rails test suites. -### Be Careful with Sensitive Information +### Prevent runner with tags from picking jobs without tags + +You can configure a runner to prevent it from picking jobs with tags when +the runnner does not have tags assigned. This setting is available on each +runner in *Project Settings* > *Runners*. + +### Be careful with sensitive information If you can run a build on a runner, you can get access to any code it runs and get the token of the runner. With shared runners, this means that anyone diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 79ed512aabb..5c316510d0e 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -33,7 +33,7 @@ POST /projects/:id/trigger/builds The required parameters are the trigger's `token` and the Git `ref` on which the trigger will be performed. Valid refs are the branch, the tag or the commit -SHA. The `:id` of a project can be found by [querying the API](../api/projects.md) +SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md) or by visiting the **Triggers** page which provides self-explanatory examples. When a rebuild is triggered, the information is exposed in GitLab's UI under diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7e9bced7616..a3481f58c6c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ### after_script >**Note:** -Introduced in GitLab 8.7 and GitLab Runner v1.2. +Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released) `after_script` is used to define the command that will be run after for all builds. This has to be an array or a multi-line string. @@ -348,7 +348,7 @@ job_name: | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| -| artifacts | no | Define list build artifacts | +| artifacts | no | Define list of build artifacts | | cache | no | Define list of files that should be cached between subsequent runs | | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md new file mode 100644 index 00000000000..4df24ef13cc --- /dev/null +++ b/doc/container_registry/README.md @@ -0,0 +1,113 @@ +# GitLab Container Registry + +> **Note:** +This feature was [introduced][ce-4040] in GitLab 8.8. + +> **Note:** +This document is about the user guide. To learn how to enable GitLab Container +Registry across your GitLab instance, visit the +[administrator documentation](../administration/container_registry.md). + +With the Docker Container Registry integrated into GitLab, every project can +have its own space to store its Docker images. + +You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. + +--- + +## Enable the Container Registry for your project + +1. First, ask your system administrator to enable GitLab Container Registry + following the [administration documentation](../administration/container_registry.md). + If you are using GitLab.com, this is enabled by default so you can start using + the Registry immediately. + +1. Go to your project's settings and enable the **Container Registry** feature + on your project. For new projects this might be enabled by default. For + existing projects you will have to explicitly enable it. + + ![Enable Container Registry](img/project_feature.png) + +## Build and push images + +After you save your project's settings, you should see a new link in the +sidebar called **Container Registry**. Following this link will get you to +your project's Registry panel where you can see how to login to the Container +Registry using your GitLab credentials. + +For example if the Registry's URL is `registry.example.com`, the you should be +able to login with: + +``` +docker login registry.example.com +``` + +Building and publishing images should be a straightforward process. Just make +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 +``` + +## 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] +``` + +For more information on running Docker containers, visit the +[Docker documentation][docker-docs]. + +## Control Container Registry from within GitLab + +GitLab offers a simple Container Registry management panel. Go to your project +and click **Container Registry** in the left sidebar. + +This view will show you all tags in your project and will easily allow you to +delete them. + +![Container Registry panel](img/container_registry.png) + +## Build and push images using GitLab CI + +> **Note:** +This feature requires GitLab 8.8 and GitLab Runner 1.2. + +Make sure that your GitLab Runner is configured to allow building docker images. +You have to check the [Using Docker Build documentation](../../ci/docker/using_docker_build.md). + +You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images, +and this is how `.gitlab-ci.yml` should look like: + +``` + build_image: + image: docker:git + services: + - docker:dind + 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 +``` + +You have to use the credentials of the special `gitlab-ci-token` user with its +password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected +to your project. This allows you to automated building and deployment of your +Docker images. + +## Limitations + +In order to use a container image from your private project as an `image:` in +your `.gitlab-ci.yml`, you have to follow the +[Using a private Docker Registry][private-docker] +documentation. This workflow will be simplified in the future. + +[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[docker-docs]: https://docs.docker.com/engine/userguide/intro/ +[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png new file mode 100644 index 00000000000..e9505a73b40 Binary files /dev/null and b/doc/container_registry/img/container_registry.png differ diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png new file mode 100644 index 00000000000..57a73d253c0 Binary files /dev/null and b/doc/container_registry/img/project_feature.png differ diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 187ec9e7b75..8292b393757 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -127,7 +127,7 @@ Inside the document: ``` If the document you are editing resides in a place other than the GitLab CE/EE `doc/` directory, instead of the relative link, use the full path: - `http://doc.gitlab.com/ce/administration/restart_gitlab.html`. + `http://docs.gitlab.com/ce/administration/restart_gitlab.html`. Replace `reconfigure` with `restart` where appropriate. ## Installation guide @@ -266,5 +266,5 @@ curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domai [cURL]: http://curl.haxx.se/ "cURL website" [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html -[gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" +[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index c1cf2e77c26..9168c70945a 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -1,12 +1,125 @@ # Instrumenting Ruby Code -GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby -code. This can be used to measure the time spent in a specific part of a larger -chunk of code. The resulting data is stored as a field in the transaction that -executed the block. +GitLab Performance Monitoring allows instrumenting of both methods and custom +blocks of Ruby code. Method instrumentation is the primary form of +instrumentation with block-based instrumentation only being used when we want to +drill down to specific regions of code within a method. -To start measuring a block of Ruby code you should use `Gitlab::Metrics.measure` -and give it a name: +## Instrumenting Methods + +Instrumenting methods is done by using the `Gitlab::Metrics::Instrumentation` +module. This module offers a few different methods that can be used to +instrument code: + +* `instrument_method`: instruments a single class method. +* `instrument_instance_method`: instruments a single instance method. +* `instrument_class_hierarchy`: given a Class this method will recursively + instrument all sub-classes (both class and instance methods). +* `instrument_methods`: instruments all public class methods of a Module. +* `instrument_instance_methods`: instruments all public instance methods of a + Module. + +To remove the need for typing the full `Gitlab::Metrics::Instrumentation` +namespace you can use the `configure` class method. This method simply yields +the supplied block while passing `Gitlab::Metrics::Instrumentation` as its +argument. An example: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_method(Foo, :bar) + conf.instrument_method(Foo, :baz) +end +``` + +Using this method is in general preferred over directly calling the various +instrumentation methods. + +Method instrumentation should be added in the initializer +`config/initializers/metrics.rb`. + +### Examples + +Instrumenting a single method: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_method(User, :find_by) +end +``` + +Instrumenting an entire class hierarchy: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_class_hierarchy(ActiveRecord::Base) +end +``` + +Instrumenting all public class methods: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_methods(User) +end +``` + +### Checking Instrumented Methods + +The easiest way to check if a method has been instrumented is to check its +source location. For example: + +``` +method = Rugged::TagCollection.instance_method(:[]) + +method.source_location +``` + +If the source location points to `lib/gitlab/metrics/instrumentation.rb` you +know the method has been instrumented. + +If you're using Pry you can use the `$` command to display the source code of a +method (along with its source location), this is easier than running the above +Ruby code. In case of the above snippet you'd run the following: + +``` +$ Rugged::TagCollection#[] +``` + +This will print out something along the lines of: + +``` +From: /path/to/your/gitlab/lib/gitlab/metrics/instrumentation.rb @ line 148: +Owner: # +Visibility: public +Number of lines: 21 + +def #{name}(#{args_signature}) + trans = Gitlab::Metrics::Instrumentation.transaction + + if trans + start = Time.now + retval = super + duration = (Time.now - start) * 1000.0 + + if duration >= Gitlab::Metrics.method_call_threshold + trans.increment(:method_duration, duration) + + trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, + { duration: duration }, + method: #{label.inspect}) + end + + retval + else + super + end +end +``` + +## Instrumenting Ruby Blocks + +Measuring blocks of Ruby code is done by calling `Gitlab::Metrics.measure` and +passing it a block. For example: ```ruby Gitlab::Metrics.measure(:foo) do @@ -14,6 +127,10 @@ Gitlab::Metrics.measure(:foo) do end ``` +The block is executed and the execution time is stored as a set of fields in the +currently running transaction. If no transaction is present the block is yielded +without measuring anything. + 3 values are measured for a block: 1. The real time elapsed, stored in NAME_real_time. diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 28dedf3978c..02e024ca15a 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -8,7 +8,10 @@ In addition, having to take a server offline for a an upgrade small or big is a big burden for most organizations. For this reason it is important that your migrations are written carefully, can be applied online and adhere to the style guide below. -It's advised to have offline migrations only in major GitLab releases. +Migrations should not require GitLab installations to be taken offline unless +_absolutely_ necessary. If a migration requires downtime this should be +clearly mentioned during the review process as well as being documented in the +monthly release post. When writing your migrations, also consider that databases might have stale data or inconsistencies and guard for that. Try to make as little assumptions as possible @@ -58,6 +61,45 @@ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation. +When adding an index make sure to use the method `add_concurrent_index` instead +of the regular `add_index` method. The `add_concurrent_index` method +automatically creates concurrent indexes when using PostgreSQL, removing the +need for downtime. To use this method you must disable transactions by calling +the method `disable_ddl_transaction!` in the body of your migration class like +so: + +``` +class MyMigration < ActiveRecord::Migration + disable_ddl_transaction! + + def change + + end +end +``` + +## Adding Columns With Default Values + +When adding columns with default values you should use the method +`add_column_with_default`. This method ensures the table is updated without +requiring downtime. This method is not reversible so you must manually define +the `up` and `down` methods in your migration class. + +For example, to add the column `foo` to the `projects` table with a default +value of `10` you'd write the following: + +``` +class MyMigration < ActiveRecord::Migration + def up + add_column_with_default(:projects, :foo, :integer, 10) + end + + def down + remove_column(:projects, :foo) + end +end +``` + ## Testing Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. @@ -74,7 +116,7 @@ Example with Arel: users = Arel::Table.new(:users) users.group(users[:user_id]).having(users[:id].count.gt(5)) -#updtae other tables with this results +#update other tables with these results ``` Example with plain SQL and `quote_string` helper: @@ -89,4 +131,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") end -``` \ No newline at end of file +``` diff --git a/doc/development/testing.md b/doc/development/testing.md index 33eed29ba5c..513457d203a 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -65,7 +65,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at - Use `context` to test branching logic. - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). - Don't supply the `:each` argument to hooks since it's the default. -- Prefer `not_to` to `to_not`. +- Prefer `not_to` to `to_not` (_this is enforced by Rubocop_). - Try to match the ordering of tests to the ordering within the class. - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines to separate phases. diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index a3e260a5f89..23760a14b39 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -6,3 +6,51 @@ We created a page inside GitLab where you can check commonly used html and css e When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples you can use during GitLab development. + +## Design repository + +All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design) +repository and maintained by GitLab UX designers. + +## Navigation + +GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu. +This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo +and the current user's profile picture. The content section contains a header and the content itself. +The header describes the current GitLab page and what navigation is +available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the +project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group. + +### Adding new tab to header navigation + +We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each +tab should represent separate functionality. Everything related to the issue +tracker should be under the 'Issues' tab while everything related to the wiki should +be under 'Wiki' tab and so on and so forth. + +## Mobile screen size + +We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide +part of the UI for smaller resolutions in favor of a better user experience. +However core functionality like browsing files, creating issues, writing comments, should +be available on all resolutions. + +## Icons + +* `trash` icon for button or link that does destructive action like removing +information from database or file system +* `x` icon for closing/hiding UI element. For example close modal window +* `pencil` icon for edit button or link +* `eye` icon for subscribe action +* `rss` for rss/atom feed +* `plus` for link or dropdown that lead to page where you create new object (For example new issue page) + + +## Buttons + +* Button should contain icon or text. Exceptions should be approved by UX designer. +* Use gray button on white background or white button on gray background. +* Use red button for destructive actions (not revertable). For example removing issue. +* Use green or blue button for primary action. Primary button should be only one. +Do not use both green and blue button in one form. + diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 87f078def04..5221d85b661 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index b545d62549d..f737dffc024 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -14,7 +14,7 @@ Fill out the required information: 1. Select a [visibility level](https://gitlab.com/help/public_access/public_access) -1. You can also [import your existing projects](http://doc.gitlab.com/ce/workflow/importing/README.html) +1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html) 1. Click on "create project" diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index dcdf49d3379..820934f97f1 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/install/installation.md b/doc/install/installation.md index e3af3022262..1318b3d1fa5 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -6,7 +6,7 @@ Since an installation from source is a lot of work and error prone we strongly r One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes. On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time. -Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. +Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. After this termination Runit will detect Sidekiq is not running and will start it. Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time. @@ -269,9 +269,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-8-stable gitlab -**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -394,7 +394,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.7.1 + sudo -u git -H git checkout v0.7.4 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md index 0245febfcd8..44d2a14f366 100644 --- a/doc/install/relative_url.md +++ b/doc/install/relative_url.md @@ -132,5 +132,5 @@ To disable the relative URL: 1. Follow the same as above starting from 2. and set up the GitLab URL to one that doesn't contain a relative path. -[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" +[omnibus-rel]: http://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" [restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/install/requirements.md b/doc/install/requirements.md index df8e8bdc476..09c6211b3ab 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -81,7 +81,7 @@ errors during usage. - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) We recommend having at least 1GB of swap on your server, even if you currently have -enough available RAM. Having swap will help reduce the chance of errors occuring +enough available RAM. Having swap will help reduce the chance of errors occurring if your available memory changes. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. @@ -150,3 +150,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) - Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled. +- Edge (Latest stable version) diff --git a/doc/integration/README.md b/doc/integration/README.md index 6fe04aa2a06..fd330dd7a7d 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -19,7 +19,7 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. -[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html +[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html ## Project services diff --git a/doc/integration/cas.md b/doc/integration/cas.md index e6b2071f193..e34e306f9ac 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -27,17 +27,18 @@ To enable the CAS OmniAuth provider you must register your application with your ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "cas3", - label: "cas", - args: { - url: 'CAS_SERVER', - login_url: '/CAS_PATH/login', - service_validate_url: '/CAS_PATH/p3/serviceValidate', - logout_url: '/CAS_PATH/logout'} } - } + "name"=> "cas3", + "label"=> "cas", + "args"=> { + "url"=> 'CAS_SERVER', + "login_url"=> '/CAS_PATH/login', + "service_validate_url"=> '/CAS_PATH/p3/serviceValidate', + "logout_url"=> '/CAS_PATH/logout' + } } ] ``` + For installations from source: @@ -57,6 +58,8 @@ To enable the CAS OmniAuth provider you must register your application with your 1. Save the configuration file. +1. Run `gitlab-ctl reconfigure` for the omnibus package. + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a CAS tab in the sign in form. diff --git a/doc/integration/google.md b/doc/integration/google.md index f9a20dd840d..82978b68a34 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -11,9 +11,9 @@ To enable the Google OAuth2 OmniAuth provider you must register your application - Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one. 1. Refresh the page. You should now see your new project in the list. Click on the project. -1. Select "APIs & auth" in the left menu. +1. Select the "Google APIs" tab in the Overview. -1. Select "APIs" in the submenu. +1. Select and enable the following Google APIs - listed under "Popular APIs" - Enable `Contacts API` - Enable `Google+ API` diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png new file mode 100644 index 00000000000..95f8bbdcd24 Binary files /dev/null and b/doc/integration/img/enabled-oauth-sign-in-sources.png differ diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index cab329c0dec..820f40f81a9 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -11,6 +11,7 @@ of the configured mechanisms. - [Supported Providers](#supported-providers) - [Enable OmniAuth for an Existing User](#enable-omniauth-for-an-existing-user) - [OmniAuth configuration sample when using Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master#omniauth-google-twitter-github-login) +- [Enable or disable Sign In with an OmniAuth provider without disabling import sources](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources) ## Supported Providers @@ -191,3 +192,17 @@ experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/w While we can't officially support every possible authentication mechanism out there, we'd like to at least help those with specific needs. + +## Enable or disable Sign In with an OmniAuth provider without disabling import sources + +>**Note:** +This setting was introduced with version 8.8 of GitLab. + +Administrators are able to enable or disable Sign In via some OmniAuth providers. + +>**Note:** +By default Sign In is enabled via all the OAuth Providers that have been configured in `config/gitlab.yml`. + +In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. + +![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) diff --git a/doc/intro/README.md b/doc/intro/README.md index ab298d3808e..382d10aaf40 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -39,4 +39,4 @@ Install and update your GitLab installation. - [Install GitLab](https://about.gitlab.com/installation/) - [Update GitLab](https://about.gitlab.com/update/) -- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html) +- [Explore Omnibus GitLab configuration options](http://docs.gitlab.com/omnibus/settings/configuration.html) diff --git a/doc/logs/logs.md b/doc/logs/logs.md index 27937e51764..f84060b8d07 100644 --- a/doc/logs/logs.md +++ b/doc/logs/logs.md @@ -1,6 +1,6 @@ ## Log system -GitLab has advanced log system so everything is logging and you can analize your instance using various system log files. -In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://doc.gitlab.com/ee/administration/audit_events.html) +GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files. +In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. @@ -67,13 +67,13 @@ gitlab-shell is using by Gitlab for executing git commands and provide ssh acces ``` I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at . -I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and simlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. +I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. ``` #### unicorn_stderr.log This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from the source. -Unicorn is a high-performance forking Web server which is used for serving GitLab application. You can look at this log, for example, if your application does not respond. This log cantains all information about state of unicorn processes at any given time. +Unicorn is a high-performance forking Web server which is used for serving the GitLab application. You can look at this log if, for example, your application does not respond. This log contains all information about the state of unicorn processes at any given time. ``` I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 4f199b6af6f..236eb7b12c4 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -8,6 +8,7 @@ * [Multiple underscores in words](#multiple-underscores-in-words) * [URL auto-linking](#url-auto-linking) * [Code and Syntax Highlighting](#code-and-syntax-highlighting) +* [Inline Diff](#inline-diff) * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) * [Task lists](#task-lists) @@ -153,6 +154,19 @@ s = "There is no highlighting for this." But let's throw in a tag. ``` +## Inline Diff + +With inline diffs tags you can display {+ additions +} or [- deletions -]. + +The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. + +However the wrapping tags cannot be mixed as such: + +- {+ additions +] +- [+ additions +} +- {- deletions -] +- [- deletions -} + ## Emoji Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: @@ -185,20 +199,23 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -| input | references | -|:-----------------------|:---------------------------| -| `@user_name` | specific user | -| `@group_name` | specific group | -| `@all` | entire team | -| `#123` | issue | -| `!123` | merge request | -| `$123` | snippet | -| `~123` | label by ID | -| `~bug` | one-word label by name | -| `~"feature request"` | multi-word label by name | -| `9ba12248` | specific commit | -| `9ba12248...b19a04f5` | commit range comparison | -| `[README](doc/README)` | repository file references | +| input | references | +|:-----------------------|:--------------------------- | +| `@user_name` | specific user | +| `@group_name` | specific group | +| `@all` | entire team | +| `#123` | issue | +| `!123` | merge request | +| `$123` | snippet | +| `~123` | label by ID | +| `~bug` | one-word label by name | +| `~"feature request"` | multi-word label by name | +| `%123` | milestone by ID | +| `%v1.23` | one-word milestone by name | +| `%"release candidate"` | multi-word milestone by name | +| `9ba12248` | specific commit | +| `9ba12248...b19a04f5` | commit range comparison | +| `[README](doc/README)` | repository file references | GFM also recognizes certain cross-project references: @@ -206,6 +223,7 @@ GFM also recognizes certain cross-project references: |:----------------------------------------|:------------------------| | `namespace/project#123` | issue | | `namespace/project!123` | merge request | +| `namespace/project%123` | milestone | | `namespace/project$123` | snippet | | `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248...b19a04f5` | commit range comparison | @@ -402,7 +420,7 @@ There are two ways to create links, inline-style and reference-style. [I'm a reference-style link][Arbitrary case-insensitive reference text] -[I'm a relative reference to a repository file](LICENSE) +[I'm a relative reference to a repository file](LICENSE)[^1] [You can use numbers for reference-style link definitions][1] @@ -594,3 +612,4 @@ By including colons in the header row, you can align the text within that column [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" +[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 5ec0a2069b5..8f9ef054949 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -355,7 +355,7 @@ sudo chown git:git /var/opt/gitlab/gitlab-ci/builds ``` #### Problems when importing CI database to GitLab -If you were migrating CI database from MySQL to PostgreSQL manually you can see errros during import about missing sequences: +If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences: ``` ALTER SEQUENCE ERROR: relation "ci_builds_id_seq" does not exist diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md new file mode 100644 index 00000000000..0d17799372f --- /dev/null +++ b/doc/monitoring/health_check.md @@ -0,0 +1,66 @@ +# Health Check + +>**Note:** This feature was [introduced][ce-3888] in GitLab 8.8. + +GitLab provides a health check endpoint for uptime monitoring on the `health_check` web +endpoint. The health check reports on the overall system status based on the status of +the database connection, the state of the database migrations, and the ability to write +and access the cache. This endpoint can be provided to uptime monitoring services like +[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health]. + +## Access Token + +An access token needs to be provided while accessing the health check endpoint. The current +accepted token can be found on the `admin/health_check` page of your GitLab instance. + +![access token](img/health_check_token.png) + +The access token can be passed as a URL parameter: + +``` +https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN +``` + +or as an HTTP header: + +```bash +curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +``` + +## Using the Endpoint + +Once you have the access token, health information can be retrieved as plain text, JSON, +or XML using the `health_check` endpoint: + +- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN` + +You can also ask for the status of specific services: + +- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN` + +For example, the JSON output of the following health check: + +```bash +curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +``` + +would be like: + +``` +{"healthy":true,"message":"success"} +``` + +## Status + +On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint +will return a valid successful HTTP status code, and a `success` message. Ideally your +uptime monitoring should look for the success message. + +[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 +[pingdom]: https://www.pingdom.com +[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html +[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring diff --git a/doc/monitoring/img/health_check_token.png b/doc/monitoring/img/health_check_token.png new file mode 100644 index 00000000000..2daf8606b00 Binary files /dev/null and b/doc/monitoring/img/health_check_token.png differ diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md index 39086b7a251..54adb99386a 100644 --- a/doc/operations/moving_repositories.md +++ b/doc/operations/moving_repositories.md @@ -134,7 +134,7 @@ sudo -u git sh -c ' cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\ /usr/bin/env JOBS=10 \ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \ - /var/opt/gitlab/transfer-logs/succes-$(date +%s).log \ + /var/opt/gitlab/transfer-logs/success-$(date +%s).log \ /var/opt/gitlab/git-data/repositories \ /mnt/gitlab/repositories ' @@ -145,7 +145,7 @@ sudo -u git -H sh -c ' cat /home/git/transfer-logs/* | sort | uniq -u |\ /usr/bin/env JOBS=10 \ bin/parallel-rsync-repos \ - /home/git/transfer-logs/succes-$(date +%s).log \ + /home/git/transfer-logs/success-$(date +%s).log \ /home/git/repositories \ /mnt/gitlab/repositories ` @@ -164,7 +164,7 @@ sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ sudo -u git \ /usr/bin/env JOBS=10 \ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \ - succes-$(date +%s).log \ + success-$(date +%s).log \ /var/opt/gitlab/git-data/repositories \ /mnt/gitlab/repositories @@ -174,7 +174,7 @@ sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ sudo -u git -H \ /usr/bin/env JOBS=10 \ bin/parallel-rsync-repos \ - succes-$(date +%s).log \ + success-$(date +%s).log \ /home/git/repositories \ /mnt/gitlab/repositories ``` diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 6219693b8a8..b76ce31cbad 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -27,6 +27,7 @@ documentation](../workflow/add-user/add-user.md). | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | | See a commit status | | ✓ | ✓ | ✓ | ✓ | +| See a container registry | | ✓ | ✓ | ✓ | ✓ | | Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | @@ -37,6 +38,8 @@ documentation](../workflow/add-user/add-user.md). | Write a wiki | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | +| Update a container registry | | | ✓ | ✓ | ✓ | +| Remove a container registry image | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 6be954ad68b..a49c43b8ef2 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -8,4 +8,4 @@ - [User management](user_management.md) - [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk -- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators +- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators diff --git a/doc/security/README.md b/doc/security/README.md index 4cd0fdd4094..38706e48ec5 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -8,3 +8,4 @@ - [User File Uploads](user_file_uploads.md) - [How we manage the CRIME vulnerability](crime_vulnerability.md) - [Enforce Two-factor authentication](two_factor_authentication.md) +- [Send email confirmation on sign-up](user_email_confirmation.md) diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md new file mode 100644 index 00000000000..4293944ae8b --- /dev/null +++ b/doc/security/user_email_confirmation.md @@ -0,0 +1,7 @@ +# User email confirmation at sign-up + +Gitlab admin can enable email confirmation on sign-up, if you want to confirm all +user emails before they are able to sign-in. + +In the Admin area under **Settings** (`/admin/application_settings`), go to section +**Sign-in Restrictions** and look for **Send confirmation email on sign-up** option. diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md index 4a2c6ea91d2..bb463d43a7c 100644 --- a/doc/update/8.6-to-8.7.md +++ b/doc/update/8.6-to-8.7.md @@ -86,6 +86,14 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ### 7. Update configuration files +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-6-stable:config/gitlab.yml.example origin/8-7-stable:config/gitlab.yml.example +``` + #### Git configuration Disable `git gc --auto` because GitLab runs `git gc` for us already. diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md new file mode 100644 index 00000000000..32906650f6f --- /dev/null +++ b/doc/update/8.7-to-8.8.md @@ -0,0 +1,162 @@ +# From 8.7 to 8.8 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-8-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v2.7.2 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-7-stable:config/gitlab.yml.example origin/8-8-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab-ssl origin/8-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab origin/8-8-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.6 to 8.7](8.6-to-8.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/README.md b/doc/update/README.md index a770633c9b8..975d72164b4 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -29,7 +29,7 @@ Based on your installation, choose a section below that fits your needs. ## Omnibus Packages -- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html) +- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) contains the steps needed to update an Omnibus GitLab package. ## Installation from source @@ -86,10 +86,10 @@ possible. information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) -[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html +[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update [source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [ee-ce]: ../downgrade_ee_to_ce/README.md [ce]: https://about.gitlab.com/features/#community [ee]: https://about.gitlab.com/features/#enterprise -[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition +[omni-ce-ee]: http://docs.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index b4283a526f3..60729316cde 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -57,10 +57,10 @@ sudo -u git -H make cd /home/git/gitlab # PostgreSQL -sudo -u git -H bundle install --without development test mysql --with postgres --deployment +sudo -u git -H bundle install --without development test mysql --deployment # MySQL -sudo -u git -H bundle install --without development test postgres --with mysql --deployment +sudo -u git -H bundle install --without development test postgres --deployment # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index c1c51302e79..8559b67af04 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -13,6 +13,19 @@ You can configure webhooks to listen for specific events like pushes, issues or Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. +## Webhook endpoint tips + +If you are writing your own endpoint (web server) that will receive +GitLab webhooks keep in mind the following things: + +- Your endpoint should send its HTTP response as fast as possible. If + you wait too long, GitLab may decide the hook failed and retry it. +- Your endpoint should ALWAYS return a valid HTTP response. If you do + not do this then GitLab will think the hook failed and retry it. + Most HTTP libraries take care of this for you automatically but if + you are writing a low-level hook this is important to remember. +- GitLab ignores the HTTP status code returned by your endpoint. + ## SSL Verification By default, the SSL certificate of the webhook endpoint is verified based on @@ -682,6 +695,61 @@ X-Gitlab-Event: Merge Request Hook } ``` +## Wiki Page events + +Triggered when a wiki page is created or edited. + +**Request Header**: + +``` +X-Gitlab-Event: Wiki Page Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "wiki_page", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project": { + "name": "awesome-project", + "description": "This is awesome", + "web_url": "http://example.com/root/awesome-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:root/awesome-project.git", + "git_http_url": "http://example.com/root/awesome-project.git", + "namespace": "root", + "visibility_level": 0, + "path_with_namespace": "root/awesome-project", + "default_branch": "master", + "homepage": "http://example.com/root/awesome-project", + "url": "git@example.com:root/awesome-project.git", + "ssh_url": "git@example.com:root/awesome-project.git", + "http_url": "http://example.com/root/awesome-project.git" + }, + "wiki": { + "web_url": "http://example.com/root/awesome-project/wikis/home", + "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", + "git_http_url": "http://example.com/root/awesome-project.wiki.git", + "path_with_namespace": "root/awesome-project.wiki", + "default_branch": "master" + }, + "object_attributes": { + "title": "Awesome", + "content": "awesome content goes here", + "format": "markdown", + "message": "adding an awesome page to the wiki", + "slug": "awesome", + "url": "http://example.com/root/awesome-project/wikis/awesome", + "action": "create" + } +} +``` + #### Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 1b354bcc0f1..2b2f140f8bf 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -187,7 +187,7 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. -In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. +In GitLab EE and .com you can also [rebase before merge](http://docs.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 52bf611dc5e..34ada1774d8 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -54,7 +54,7 @@ If necessary, you can increase the access level of an individual user for a spec ## Managing group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. -See [the GitLab Enterprise Edition documentation](http://doc.gitlab.com/ee/integration/ldap.html) for more information. +See [the GitLab Enterprise Edition documentation](http://docs.gitlab.com/ee/integration/ldap.html) for more information. ## Allowing only admins to create groups diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index e670e415c71..a7dfac2c120 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -44,5 +44,5 @@ case the namespace is taken, the project will be imported on the user's namespace. [gh-import]: ../../integration/github.md "GitHub integration" -[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" +[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE" [new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index 1117db98e7e..dcc00074b75 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -2,7 +2,7 @@ You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if GitLab support is enabled on your GitLab instance. -You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html) +You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. ![New project page](gitlab_importer/new_project_page.png) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 31620044b15..9fe065fa680 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -4,7 +4,7 @@ Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. The general recommendation is to not have Git repositories larger than 1GB to preserve performance. -GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) +GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain environments it is not always convenient to use different commands to differentiate between the large files and regular ones. @@ -127,7 +127,7 @@ To prevent this from happening, set the lfs url in project Git config: ```bash -git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch" +git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs" ``` ### Credentials are always required when pushing an object @@ -152,4 +152,4 @@ If you are using OS X you can use `osxkeychain` to store and encrypt your creden For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). More details about various methods of storing the user credentials can be found -on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file +on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 80817c98d22..cbca94c0b5e 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -69,7 +69,7 @@ In all of the below cases, the notification will be sent to: ...with notification level "Participating" or higher -- Watchers: project members with notification level "Watch" +- Watchers: users with notification level "Watch" - Subscribers: anyone who manually subscribed to the issue/merge request | Event | Sent to | diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 2fd097d100b..26e67503021 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,14 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Files + Scenario: On Project Code Given I visit my project's files page - Then the active main tab should be Files - And no other main tabs should be active - - Scenario: On Project Commits - Given I visit my project's commits page - Then the active main tab should be Commits + Then the active main tab should be Code And no other main tabs should be active Scenario: On Project Issues @@ -30,11 +25,6 @@ Feature: Project Active Tab Then the active main tab should be Merge Requests And no other main tabs should be active - Scenario: On Project Members - Given I visit my project's members page - Then the active main tab should be Members - And no other main tabs should be active - Scenario: On Project Wiki Given I visit my project's wiki page Then the active main tab should be Wiki @@ -49,13 +39,6 @@ Feature: Project Active Tab # Sub Tabs: Settings - Scenario: On Project Settings/Edit - Given I visit my project's settings page - And I click the "Edit" tab - Then the active sub nav should be Edit - And no other sub navs should be active - And the active main tab should be Settings - Scenario: On Project Settings/Hooks Given I visit my project's settings page And I click the "Hooks" tab @@ -70,40 +53,52 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Commits + Scenario: On Project Members + Given I visit my project's members page + Then the active sub nav should be Members + And no other sub navs should be active + And the active main tab should be Settings + + # Sub Tabs: Code + + Scenario: On Project Code/Files + Given I visit my project's files page + Then the active sub tab should be Files + And no other sub tabs should be active + And the active main tab should be Code - Scenario: On Project Commits/Commits + Scenario: On Project Code/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Network + Scenario: On Project Code/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Compare + Scenario: On Project Code/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Branches + Scenario: On Project Code/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Tags + Scenario: On Project Code/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index 3c029a973df..550ebccf0d7 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -24,3 +24,4 @@ Feature: Project Builds Summary Then recent build has been erased And recent build summary does not have artifacts widget And recent build summary contains information saying that build has been erased + And the build count cache is updated diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature deleted file mode 100644 index a4be39b2d40..00000000000 --- a/features/project/commits/tags.feature +++ /dev/null @@ -1,46 +0,0 @@ -@project_commits -Feature: Project Commits Tags - Background: - Given I sign in as a user - And I own project "Shop" - Given I visit project tags page - - Scenario: I can see all git tags - Then I should see "Shop" all tags list - - Scenario: I create a tag - And I click new tag link - And I submit new tag form - Then I should see new tag created - - Scenario: I create a tag with release notes - Given I click new tag link - And I submit new tag form with release notes - Then I should see new tag created - And I should see tag release notes - - Scenario: I create a tag with invalid name - And I click new tag link - And I submit new tag form with invalid name - Then I should see new an error that tag is invalid - - Scenario: I create a tag with invalid reference - And I click new tag link - And I submit new tag form with invalid reference - Then I should see new an error that tag ref is invalid - - Scenario: I create a tag that already exists - And I click new tag link - And I submit new tag form with tag that already exists - Then I should see new an error that tag already exists - - Scenario: I delete a tag - Given I visit tag 'v1.1.0' page - Given I delete tag 'v1.1.0' - Then I should not see tag 'v1.1.0' - - Scenario: I add release notes to the tag - Given I visit tag 'v1.1.0' page - When I click edit tag link - And I fill release notes and submit form - Then I should see tag release notes diff --git a/features/project/create.feature b/features/project/create.feature index 27136798e36..67336d73bf7 100644 --- a/features/project/create.feature +++ b/features/project/create.feature @@ -6,21 +6,9 @@ Feature: Project Create @javascript Scenario: User create a project - Given I sign in as a user - When I visit new project page - And I have an ssh key - And fill project form with valid data - Then I should see project page - And I should see empty project instuctions - - @javascript - Scenario: Empty project instructions Given I sign in as a user And I have an ssh key When I visit new project page And fill project form with valid data - Then I see empty project instuctions - And I click on HTTP - Then Remote url should update to http link - And If I click on SSH - Then Remote url should update to ssh link + Then I should see project page + And I should see empty project instructions diff --git a/features/project/project.feature b/features/project/project.feature index f1f3ed26065..aa22401c88e 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,15 +18,6 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button - Scenario: I should have back to group button - And project "Shop" belongs to group - And I visit project "Shop" page - Then I should see back to group button - - Scenario: I should have back to group button - And I visit project "Shop" page - Then I should see back to dashboard button - Scenario: I should have readme on page And I visit project "Shop" page Then I should see project "Shop" README diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 10e7c234610..c73d0b32337 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,19 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Files + Then the active main tab should be Code + Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Commits + Then the active main tab should be Code + Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Commits + And the active main tab should be Code @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb index 90d13abdb13..f2db1801389 100644 --- a/features/steps/admin/active_tab.rb +++ b/features/steps/admin/active_tab.rb @@ -1,7 +1,7 @@ class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps include SharedAuthentication include SharedPaths - include SharedActiveTab + include SharedSidebarActiveTab step 'the active main tab should be Home' do ensure_active_main_tab('Overview') @@ -34,4 +34,12 @@ class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps step 'the active main tab should be Messages' do ensure_active_main_tab('Messages') end + + step 'no other main tabs should be active' do + expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar > li.active')).to have_content(content) + end end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index 4bc290b6bdf..8fb8a86d58b 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -158,7 +158,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps step 'I should not see twitter details' do expect(page).to have_content 'Pete' - expect(page).to_not have_content 'twitter' + expect(page).not_to have_content 'twitter' end step 'click on ssh keys tab' do diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb index 0e2c04fb299..04fe96cef22 100644 --- a/features/steps/dashboard/active_tab.rb +++ b/features/steps/dashboard/active_tab.rb @@ -1,9 +1,5 @@ class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps include SharedAuthentication include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Help' do - ensure_active_main_tab('Help') - end + include SharedSidebarActiveTab end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index b5980b35102..80ed4c6d64c 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see "Shop" project CI status' do - expect(page).to have_link "Build skipped" + expect(page).to have_link "Commit: skipped" end step 'I should see last push widget' do diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index e21af72a777..8706f0e8e78 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -74,7 +74,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index a2adc87f8ef..06db36c7014 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -100,7 +100,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb index a9083850b52..118d27888df 100644 --- a/features/steps/dashboard/shortcuts.rb +++ b/features/steps/dashboard/shortcuts.rb @@ -2,5 +2,6 @@ class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedProject - include SharedActiveTab + include SharedSidebarActiveTab + include SharedShortcuts end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 2b23df6764b..bd8a270202e 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -20,7 +20,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps step 'I have todos' do create(:todo, user: current_user, project: project, author: mary_jane, target: issue, action: Todo::MENTIONED) create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::ASSIGNED) - note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?") + note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?", project: project) create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::MENTIONED, note: note) create(:todo, user: current_user, project: project, author: john_doe, target: merge_request, action: Todo::ASSIGNED) end @@ -106,7 +106,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps if pending expect(page).to have_link 'Done' else - expect(page).to_not have_link 'Done' + expect(page).not_to have_link 'Done' end end end diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 3b59089a093..4724a326277 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -22,8 +22,4 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps step 'the active main tab should be Audit Log' do ensure_active_main_tab('Audit Log') end - - def ensure_active_main_tab(content) - expect(find('.layout-nav li.active')).to have_content(content) - end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 909de31a479..b1a87b96efd 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -166,7 +166,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I have group with projects' do - @group = create(:group) + @group = create(:group) @group.add_owner(current_user) @project = create(:project, namespace: @group) @event = create(:closed_issue_event, project: @project) diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 19d81453d8c..745fd3471c4 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -16,12 +16,14 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Snippets" tab' do - click_link('Snippets') + page.within('.layout-nav') do + click_link('Snippets') + end end - step 'I click the "Edit" tab' do - page.within '.sidebar-subnav' do - click_link('Project Settings') + step 'I click the "Edit Project"' do + page.within '.layout-nav .controls' do + click_link('Edit Project') end end @@ -33,14 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Deploy Keys') end - step 'the active sub nav should be Team' do + step 'the active sub nav should be Members' do ensure_active_sub_nav('Members') end - step 'the active sub nav should be Edit' do - ensure_active_sub_nav('Project') - end - step 'the active sub nav should be Hooks' do ensure_active_sub_nav('Webhooks') end @@ -56,17 +54,15 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Branches" tab' do - click_link('Branches') + page.within '.content' do + click_link('Branches') + end end step 'I click the "Tags" tab' do click_link('Tags') end - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end @@ -82,11 +78,15 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues step 'I click the "Milestones" tab' do - click_link('Milestones') + page.within('.layout-nav') do + click_link('Milestones') + end end step 'I click the "Labels" tab' do - click_link('Labels') + page.within('.layout-nav') do + click_link('Labels') + end end step 'the active sub tab should be Issues' do diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index e9e2359146e..374eb0b0e07 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -36,4 +36,8 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps expect(page).to have_content 'Build has been erased' end end + + step 'the build count cache is updated' do + expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) + end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 93c37bf507f..e1b29f1e57a 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -105,7 +105,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I should not see button to create a new merge request' do - expect(page).to_not have_link 'Create Merge Request' + expect(page).not_to have_link 'Create Merge Request' end step 'I should see button to the merge request' do @@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see commit ci info' do - expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for 1 pipeline pending" end step 'I click status link' do @@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see builds list' do - expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for 1 pipeline pending" expect(page).to have_content "1 build" end diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb deleted file mode 100644 index 912ba580efd..00000000000 --- a/features/steps/project/commits/tags.rb +++ /dev/null @@ -1,90 +0,0 @@ -class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I should see "Shop" all tags list' do - expect(page).to have_content "Tags" - expect(page).to have_content "v1.0.0" - end - - step 'I click new tag link' do - click_link 'New tag' - end - - step 'I submit new tag form' do - fill_in 'tag_name', with: 'v7.0' - fill_in 'ref', with: 'master' - click_button 'Create tag' - end - - step 'I submit new tag form with release notes' do - fill_in 'tag_name', with: 'v7.0' - fill_in 'ref', with: 'master' - fill_in 'release_description', with: 'Awesome release notes' - click_button 'Create tag' - end - - step 'I fill release notes and submit form' do - fill_in 'release_description', with: 'Awesome release notes' - click_button 'Save changes' - end - - step 'I submit new tag form with invalid name' do - fill_in 'tag_name', with: 'v 1.0' - fill_in 'ref', with: 'master' - click_button 'Create tag' - end - - step 'I submit new tag form with invalid reference' do - fill_in 'tag_name', with: 'foo' - fill_in 'ref', with: 'foo' - click_button 'Create tag' - end - - step 'I submit new tag form with tag that already exists' do - fill_in 'tag_name', with: 'v1.0.0' - fill_in 'ref', with: 'master' - click_button 'Create tag' - end - - step 'I should see new tag created' do - expect(page).to have_content 'v7.0' - end - - step 'I should see new an error that tag is invalid' do - expect(page).to have_content 'Tag name invalid' - end - - step 'I should see new an error that tag ref is invalid' do - expect(page).to have_content 'Target foo is invalid' - end - - step 'I should see new an error that tag already exists' do - expect(page).to have_content 'Tag v1.0.0 already exists' - end - - step "I visit tag 'v1.1.0' page" do - click_link 'v1.1.0' - end - - step "I delete tag 'v1.1.0'" do - page.within('.content') do - first('.btn-remove').click - end - end - - step "I should not see tag 'v1.1.0'" do - page.within '.tags' do - expect(page).not_to have_link 'v1.1.0' - end - end - - step 'I click edit tag link' do - click_link 'Edit release notes' - end - - step 'I should see tag release notes' do - expect(page).to have_content 'Awesome release notes' - end -end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 422b151eaa2..5f5f806df36 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -13,33 +13,9 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps expect(current_path).to eq namespace_project_path(Project.last.namespace, Project.last) end - step 'I should see empty project instuctions' do + step 'I should see empty project instructions' do expect(page).to have_content "git init" expect(page).to have_content "git remote" expect(page).to have_content Project.last.url_to_repo end - - step 'I see empty project instuctions' do - expect(page).to have_content "git init" - expect(page).to have_content "git remote" - expect(page).to have_content Project.last.url_to_repo - end - - step 'I click on HTTP' do - find('#clone-dropdown').click - find('.http-selector').click - end - - step 'Remote url should update to http link' do - expect(page).to have_content "git remote add origin #{Project.last.http_url_to_repo}" - end - - step 'If I click on SSH' do - find('#clone-dropdown').click - find('.ssh-selector').click - end - - step 'Remote url should update to ssh link' do - expect(page).to have_content "git remote add origin #{Project.last.url_to_repo}" - end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 527f7853da9..8abeb5ee242 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I goto the Merge Requests page' do - page.within '.page-sidebar-expanded' do + page.within '.layout-nav' do click_link "Merge Requests" end end diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index b1ffe7f7b4c..13c0713669a 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -59,7 +59,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps step 'hook should be triggered' do expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project) expect(page).to have_selector '.flash-notice', - text: 'Hook successfully executed.' + text: 'Hook executed successfully: HTTP 200' end step 'I should see hook error message' do diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index c5d45709b44..1b14659b4df 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -39,8 +39,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps step 'I can see the activity and food categories' do page.within '.emoji-menu' do - expect(page).to_not have_selector 'Activity' - expect(page).to_not have_selector 'Food' + expect(page).not_to have_selector 'Activity' + expect(page).not_to have_selector 'Food' end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index fc12843ea5c..5cd431e05d5 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -216,7 +216,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps page.within 'li.issue:nth-child(3)' do expect(page).to have_content 'Bugfix' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -235,7 +235,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps page.within 'li.issue:nth-child(3)' do expect(page).to have_content 'Bugfix' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -348,7 +348,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'another user adds a comment with text "Yay!" to issue "Release 0.4"' do issue = Issue.find_by!(title: 'Release 0.4') - create(:note_on_issue, noteable: issue, note: 'Yay!') + create(:note_on_issue, noteable: issue, project: project, note: 'Yay!') end step 'I should see a new comment with text "Yay!"' do diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 0ca2d6257c3..8d87f6a7a58 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -24,8 +24,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I should see labels help message' do page.within '.labels' do - expect(page).to have_content 'Create first label or generate default set of '\ - 'labels' + expect(page).to have_content 'Create a label or generate a default set '\ + 'of labels' end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 3b1a00f628a..b30346790eb 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -203,7 +203,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within 'li.merge-request:nth-child(3)' do expect(page).to have_content 'Bug NS-05' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -222,7 +222,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within 'li.merge-request:nth-child(3)' do expect(page).to have_content 'Bug NS-05' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -273,7 +273,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do mr = MergeRequest.find_by(title: "Bug NS-05") create(:note_on_merge_request_diff, project: project, - noteable_id: mr.id, + noteable: mr, author: user_exists("John Doe"), line_code: sample_commit.line_code, note: 'Line is wrong') @@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see merge request "Bug NS-05" with CI status' do page.within ".mr-list" do - expect(page).to have_link "Build pending" + expect(page).to have_link "Pipeline: pending" end end @@ -567,7 +567,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_diff_line(sample_compare.changes[1][:line_code]) end - def have_visible_content (text) + def have_visible_content(text) have_css("*", text: text, visible: true) end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index ef185861e00..a1785311c2b 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should not see "Snippets" button' do - expect(page).not_to have_link 'Snippets' + page.within '.content' do + expect(page).not_to have_link 'Snippets' + end end step 'project "Shop" belongs to group' do @@ -123,14 +125,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps @project.save! end - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - - step 'I should see back to group button' do - expect(page).to have_content 'Go to group' - end - step 'I click notifications drop down button' do click_link 'notifications-button' end diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 8c1d09d6cc6..47de4b91df1 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb index 2508c09e36d..1864b3a2b52 100644 --- a/features/steps/project/project_milestone.rb +++ b/features/steps/project/project_milestone.rb @@ -52,7 +52,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps end step 'I click link "Labels"' do - page.within('.nav-links') do + page.within('.layout-nav .nav-links') do page.find(:xpath, "//a[@href='#tab-labels']").click end end diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb index 49e9c5520bb..8143b01ca40 100644 --- a/features/steps/project/project_shortcuts.rb +++ b/features/steps/project/project_shortcuts.rb @@ -3,6 +3,7 @@ class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedProjectTab + include SharedShortcuts step 'I press "g" and "f"' do find('body').native.send_key('g') diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 786a0cad975..beb8ecfc799 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -43,12 +43,12 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps step 'I click link "Edit"' do page.within ".detail-page-header" do - click_link "Edit" + first(:link, "Edit").click end end step 'I click link "Delete"' do - click_link "Delete" + first(:link, "Delete").click end step 'I submit new snippet "Snippet three"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index c26d7a15212..2c0498de3b9 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -337,13 +337,15 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see buttons for allowed commands' do - expect(page).to have_content 'Raw' - expect(page).to have_content 'History' - expect(page).to have_content 'Permalink' - expect(page).not_to have_content 'Edit' - expect(page).not_to have_content 'Blame' - expect(page).to have_content 'Delete' - expect(page).to have_content 'Replace' + page.within '.content' do + expect(page).to have_content 'Raw' + expect(page).to have_content 'History' + expect(page).to have_content 'Permalink' + expect(page).not_to have_content 'Edit' + expect(page).not_to have_content 'Blame' + expect(page).to have_content 'Delete' + expect(page).to have_content 'Replace' + end end step 'I should see a notice about a new fork having been created' do diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 3fbcf770b62..c6ced747370 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -126,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I share project with group "OpenSource"' do project = Project.find_by(name: 'Shop') - os_group = create(:group, name: 'OpenSource') + os_group = create(:group, name: 'OpenSource') create(:project, group: os_group) @os_user1 = create(:user) @os_user2 = create(:user) diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 0bee91d758d..ace717b9909 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -2,7 +2,7 @@ module SharedActiveTab include Spinach::DSL def ensure_active_main_tab(content) - expect(find('.nav-sidebar > li.active')).to have_content(content) + expect(find('.layout-nav li.active')).to have_content(content) end def ensure_active_sub_tab(content) @@ -10,11 +10,11 @@ module SharedActiveTab end def ensure_active_sub_nav(content) - expect(find('.sidebar-subnav > li.active')).to have_content(content) + expect(find('.layout-nav .controls li.active')).to have_content(content) end step 'no other main tabs should be active' do - expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) end step 'no other sub tabs should be active' do @@ -22,26 +22,6 @@ module SharedActiveTab end step 'no other sub navs should be active' do - expect(page).to have_selector('.sidebar-subnav > li.active', count: 1) - end - - step 'the active main tab should be Home' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Projects' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Issues' do - ensure_active_main_tab('Issues') - end - - step 'the active main tab should be Merge Requests' do - ensure_active_main_tab('Merge Requests') - end - - step 'the active main tab should be Help' do - ensure_active_main_tab('Help') + expect(page).to have_selector('.layout-nav .controls li.active', count: 1) end end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index e846c52d474..e8b1e4b4879 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[id$='#{sample_commit.del_line_code}']") do + page.within("form[id$='#{sample_commit.del_line_code}-true']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in 'note[note]', with: ':smile:' click_button('Comment') end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index a58b3cb7e16..733e80b7279 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -111,7 +111,7 @@ module SharedIssuable step 'I sort the list by "Oldest updated"' do find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link "Oldest updated" end end @@ -119,7 +119,7 @@ module SharedIssuable step 'I sort the list by "Least popular"' do find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Least popular' end end @@ -127,13 +127,13 @@ module SharedIssuable step 'I sort the list by "Most popular"' do find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Most popular' end end step 'The list should be sorted by "Oldest updated"' do - page.within('div.dropdown.inline.prepend-left-10') do + page.within('.content div.dropdown.inline.prepend-left-10') do expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated') end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index a3c3887ab46..3d7c6ef9d2d 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -107,7 +107,7 @@ module SharedNote end step 'I should see no notes at all' do - expect(page).to_not have_css('.note') + expect(page).not_to have_css('.note') end # Markdown diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index ea5f9580308..ce9ea7ee18a 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -95,7 +95,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) expect(page).to have_content("Project name") - expect(page).to have_content("Features:") + expect(page).to have_content("Features") end def current_project diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 4fc2ece79ff..bfee8793301 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,12 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Files' do - ensure_active_main_tab('Files') - end - - step 'the active main tab should be Commits' do - ensure_active_main_tab('Commits') + step 'the active main tab should be Code' do + ensure_active_main_tab('Code') end step 'the active main tab should be Graphs' do @@ -41,9 +37,7 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - page.within '.nav-sidebar' do - expect(page).to have_content('Go to project') - end + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0) end step 'the active main tab should be Activity' do @@ -53,4 +47,12 @@ module SharedProjectTab step 'the active sub tab should be Network' do ensure_active_sub_tab('Network') end + + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + + step 'the active sub tab should be Commits' do + ensure_active_sub_tab('Commits') + end end diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb index bbb7afec0ad..a75a8474d26 100644 --- a/features/steps/shared/shortcuts.rb +++ b/features/steps/shared/shortcuts.rb @@ -1,4 +1,4 @@ -module SharedActiveTab +module SharedShortcuts include Spinach::DSL step 'I press "g" and "p"' do diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb new file mode 100644 index 00000000000..5c47238777f --- /dev/null +++ b/features/steps/shared/sidebar_active_tab.rb @@ -0,0 +1,35 @@ +module SharedSidebarActiveTab + include Spinach::DSL + + step 'the active main tab should be Help' do + ensure_active_main_tab('Help') + end + + step 'no other main tabs should be active' do + expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end + + step 'the active main tab should be Home' do + ensure_active_main_tab('Projects') + end + + step 'the active main tab should be Projects' do + ensure_active_main_tab('Projects') + end + + step 'the active main tab should be Issues' do + ensure_active_main_tab('Issues') + end + + step 'the active main tab should be Merge Requests' do + ensure_active_main_tab('Merge Requests') + end + + step 'the active main tab should be Help' do + ensure_active_main_tab('Help') + end +end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 023032e679f..19366b11071 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -14,12 +14,12 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps step 'I click link "Edit"' do page.within ".detail-page-header" do - click_link "Edit" + first(:link, "Edit").click end end step 'I click link "Delete"' do - click_link "Delete" + first(:link, "Delete").click end step 'I submit new snippet "Personal snippet three"' do diff --git a/features/steps/user.rb b/features/steps/user.rb index b1d088f07f9..59385a6ab59 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -34,7 +34,7 @@ class Spinach::Features::User < Spinach::FeatureSteps end step 'I should see contributions calendar' do - expect(page).to have_css('.cal-heatmap-container') + expect(page).to have_css('.js-contrib-calendar') end def contributed_project diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb new file mode 100644 index 00000000000..27acc75dcc4 --- /dev/null +++ b/generator_templates/active_record/migration/create_table_migration.rb @@ -0,0 +1,35 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # 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 :<%= table_name %> do |t| +<% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> +<% end -%> +<% end -%> +<% if options[:timestamps] %> + t.timestamps null: false +<% end -%> + end +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> +<% end -%> + end +end diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb new file mode 100644 index 00000000000..06bdea11367 --- /dev/null +++ b/generator_templates/active_record/migration/migration.rb @@ -0,0 +1,55 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # 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! + +<%- if migration_action == 'add' -%> + def change +<% attributes.each do |attribute| -%> + <%- if attribute.reference? -%> + add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + <%- end -%> +<%- end -%> + end +<%- elsif migration_action == 'join' -%> + def change + create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t| + <%- attributes.each do |attribute| -%> + <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + end + end +<%- else -%> + def change +<% attributes.each do |attribute| -%> +<%- if migration_action -%> + <%- if attribute.reference? -%> + remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + <%- if attribute.has_index? -%> + remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- end -%> +<%- end -%> +<%- end -%> + end +<%- end -%> +end diff --git a/lib/api/api.rb b/lib/api/api.rb index cc1004f8005..6cd909f6115 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,5 +1,3 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - module API class API < Grape::API include APIGuard @@ -25,38 +23,41 @@ module API format :json content_type :txt, "text/plain" - helpers Helpers - - mount Groups - mount GroupMembers - mount Users - mount Projects - mount Repositories - mount Issues - mount Milestones - mount Session - mount MergeRequests - mount Notes - mount Internal - mount SystemHooks - mount ProjectSnippets - mount ProjectMembers - mount DeployKeys - mount ProjectHooks - mount Services - mount Files - mount Commits - mount CommitStatus - mount Namespaces - mount Branches - mount Labels - mount Settings - mount Keys - mount Tags - mount Triggers - mount Builds - mount Variables - mount Runners - mount Licenses + # Ensure the namespace is right, otherwise we might load Grape::API::Helpers + helpers ::API::Helpers + + mount ::API::Groups + mount ::API::GroupMembers + mount ::API::Users + mount ::API::Projects + mount ::API::Repositories + mount ::API::Issues + mount ::API::Milestones + mount ::API::Session + mount ::API::MergeRequests + mount ::API::Notes + mount ::API::Internal + mount ::API::SystemHooks + mount ::API::ProjectSnippets + mount ::API::ProjectMembers + mount ::API::DeployKeys + mount ::API::ProjectHooks + mount ::API::Services + mount ::API::Files + mount ::API::Commits + mount ::API::CommitStatuses + mount ::API::Namespaces + mount ::API::Branches + mount ::API::Labels + mount ::API::Settings + mount ::API::Keys + mount ::API::Tags + mount ::API::Triggers + mount ::API::Builds + mount ::API::Variables + mount ::API::Runners + mount ::API::Licenses + mount ::API::Subscriptions + mount ::API::Gitignores end end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index b9994fcefda..7e67edb203a 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,171 +2,175 @@ require 'rack/oauth2' -module APIGuard - extend ActiveSupport::Concern +module API + module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end + # Must yield access token to store it in the env + request.access_token + end - helpers HelperMethods + helpers HelperMethods - install_error_responders(base) - end + install_error_responders(base) + end - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError - - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. + # + # If token is presented and valid, then it sets @current_user. + # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError + + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end end end - end - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end end end - end - def current_user - @current_user - end + def current_user + @current_user + end - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end + private - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) end end - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end + private - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) - end + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] - response.finish + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish + end end end - end - # - # Exceptions - # + # + # Exceptions + # - class MissingTokenError < StandardError; end + class MissingTokenError < StandardError; end - class TokenNotFoundError < StandardError; end + class TokenNotFoundError < StandardError; end - class ExpiredError < StandardError; end + class ExpiredError < StandardError; end - class RevokedError < StandardError; end + class RevokedError < StandardError; end - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes + end end end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 7388ed2f4ea..9bcd33ff19e 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -2,7 +2,7 @@ require 'mime/types' module API # Project commit statuses API - class CommitStatus < Grape::API + class CommitStatuses < Grape::API resource :projects do before { authenticate! } diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 93a3a5ce089..4a11c8e3620 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -107,6 +107,8 @@ module API break if opts[:line_code] end + + opts[:type] = LegacyDiffNote.name if opts[:line_code] end note = ::Notes::CreateService.new(user_project, current_user, opts).execute diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 716ca6f7ed9..790a1869f73 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,8 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled + expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id expose :namespace @@ -170,10 +171,10 @@ module API expose :label_names, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic - expose :subscribed do |issue, options| issue.subscribed?(options[:current_user]) end + expose :user_notes_count end class MergeRequest < ProjectEntity @@ -187,10 +188,10 @@ module API expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds expose :merge_status - expose :subscribed do |merge_request, options| merge_request.subscribed?(options[:current_user]) end + expose :user_notes_count end class MergeRequestChanges < MergeRequest @@ -227,9 +228,9 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_name } - expose(:line) { |note| note.diff_new_line } - expose(:line_type) { |note| note.diff_line_type } + expose(:path) { |note| note.diff_file_path if note.legacy_diff_note? } + expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } + expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } expose :author, using: Entities::UserBasic expose :created_at end @@ -307,6 +308,10 @@ module API class Label < Grape::Entity expose :name, :color, :description expose :open_issues_count, :closed_issues_count, :open_merge_requests_count + + expose :subscribed do |label, options| + label.subscribed?(options[:current_user]) + end end class Compare < Grape::Entity @@ -357,6 +362,7 @@ module API expose :restricted_signup_domains expose :user_oauth_applications expose :after_sign_out_path + expose :container_registry_token_expire_delay end class Release < Grape::Entity @@ -403,6 +409,7 @@ module API class RunnerDetails < Runner expose :tag_list + expose :run_untagged expose :version, :revision, :platform, :architecture expose :contacted_at expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } @@ -451,5 +458,13 @@ module API expose(:limitations) { |license| license.meta['limitations'] } expose :content end + + class GitignoresList < Grape::Entity + expose :name + end + + class Gitignore < Grape::Entity + expose :name, :content + end end end diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb new file mode 100644 index 00000000000..270c9501dd2 --- /dev/null +++ b/lib/api/gitignores.rb @@ -0,0 +1,29 @@ +module API + class Gitignores < Grape::API + + # Get the list of the available gitignore templates + # + # Example Request: + # GET /gitignores + get 'gitignores' do + present Gitlab::Gitignore.all, with: Entities::GitignoresList + end + + # Get the text for a specific gitignore + # + # Parameters: + # name (required) - The name of a license + # + # Example Request: + # GET /gitignores/Elixir + # + get 'gitignores/:name' do + required_attributes! [:name] + + gitignore = Gitlab::Gitignore.find(params[:name]) + not_found!('.gitignore') unless gitignore + + present gitignore, with: Entities::Gitignore + end + end +end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 91e420832f3..9d8b8d737a9 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -95,8 +95,7 @@ module API # GET /groups/:id/projects get ":id/projects" do group = find_group(params[:id]) - projects = group.projects - projects = filter_projects(projects) + projects = GroupProjectsFinder.new(group).execute(current_user) projects = paginate projects present projects, with: Entities::Project end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 40c967453fb..2aaa0557ea3 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -2,7 +2,7 @@ module API module Helpers PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token - SUDO_HEADER ="HTTP_SUDO" + SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo def parse_boolean(value) @@ -29,7 +29,7 @@ module API @current_user end - def sudo_identifier() + def sudo_identifier identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] # Regex for integers @@ -95,6 +95,17 @@ module API end end + def find_project_label(id) + label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id) + label || not_found!('Label') + end + + def find_project_issue(id) + issue = user_project.issues.find(id) + not_found! unless can?(current_user, :read_issue, issue) + issue + end + def paginate(relation) relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| add_pagination_headers(data) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 40928749481..f59a4d6c012 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -103,8 +103,7 @@ module API # Example Request: # GET /projects/:id/issues/:issue_id get ":id/issues/:issue_id" do - @issue = user_project.issues.find(params[:issue_id]) - not_found! unless can?(current_user, :read_issue, @issue) + @issue = find_project_issue(params[:issue_id]) present @issue, with: Entities::Issue, current_user: current_user end @@ -234,42 +233,6 @@ module API authorize!(:destroy_issue, issue) issue.destroy end - - # Subscribes to a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # POST /projects/:id/issues/:issue_id/subscription - post ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - not_modified! - else - issue.toggle_subscription(current_user) - present issue, with: Entities::Issue, current_user: current_user - end - end - - # Unsubscribes from a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # DELETE /projects/:id/issues/:issue_id/subscription - delete ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - issue.unsubscribe(current_user) - present issue, with: Entities::Issue, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 4af6bef0fa7..c806829d69e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -11,7 +11,7 @@ module API # Example Request: # GET /projects/:id/labels get ':id/labels' do - present user_project.labels, with: Entities::Label + present user_project.labels, with: Entities::Label, current_user: current_user end # Creates a new label @@ -36,7 +36,7 @@ module API label = user_project.labels.create(attrs) if label.valid? - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end @@ -90,7 +90,7 @@ module API attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name) if label.update(attrs) - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end diff --git a/lib/api/licenses.rb b/lib/api/licenses.rb index 187d2c04703..be0e113fbcb 100644 --- a/lib/api/licenses.rb +++ b/lib/api/licenses.rb @@ -2,15 +2,15 @@ module API # Licenses API class Licenses < Grape::API PROJECT_TEMPLATE_REGEX = - /[\<\{\[] - (project|description| - one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]/xi.freeze + /[\<\{\[] + (project|description| + one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here + [\>\}\]]/xi.freeze YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze FULLNAME_TEMPLATE_REGEX = - /[\<\{\[] - (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]/xi.freeze + /[\<\{\[] + (fullname|name\sof\s(author|copyright\sowner)) + [\>\}\]]/xi.freeze # Get the list of the available license templates # diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7e78609ecb9..4e7de8867b4 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -327,42 +327,6 @@ module API issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: Entities::Issue, current_user: current_user end - - # Subscribes to a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # POST /projects/:id/issues/:merge_request_id/subscription - post "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - not_modified! - else - merge_request.toggle_subscription(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - end - end - - # Unsubscribes from a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # DELETE /projects/:id/merge_requests/:merge_request_id/subscription - delete "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - merge_request.unsubscribe(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 71a53e6f0d6..d4fcfd3d4d3 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -19,20 +19,24 @@ module API # GET /projects/:id/issues/:noteable_id/notes # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) - - # We exclude notes that are cross-references and that cannot be viewed - # by the current user. By doing this exclusion at this level and not - # at the DB query level (which we cannot in that case), the current - # page can have less elements than :per_page even if - # there's more than one page. - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(@noteable.notes). - reject { |n| n.cross_reference_not_visible_for?(current_user) } - present notes, with: Entities::Note + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) + + if can?(current_user, noteable_read_ability_name(@noteable), @noteable) + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(@noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: Entities::Note + else + not_found!("Notes") + end end # Get a single +noteable+ note @@ -45,13 +49,14 @@ module API # GET /projects/:id/issues/:noteable_id/notes/:note_id # GET /projects/:id/snippets/:noteable_id/notes/:note_id get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) - if @note.cross_reference_not_visible_for?(current_user) - not_found!("Note") - else + if can_read_note present @note, with: Entities::Note + else + not_found!("Note") end end @@ -136,5 +141,11 @@ module API end end end + + helpers do + def noteable_read_ability_name(noteable) + "read_#{noteable.class.to_s.underscore.downcase}".to_sym + end + end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index cc2c7a0c503..5a22d14988f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -44,7 +44,7 @@ module API # Example Request: # GET /projects/starred get '/starred' do - @projects = current_user.starred_projects + @projects = current_user.viewable_starred_projects @projects = filter_projects(@projects) @projects = paginate @projects present @projects, with: Entities::Project @@ -94,6 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -112,6 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :container_registry_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -143,6 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -206,6 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -222,6 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :container_registry_enabled, :shared_runners_enabled, :public, :visibility_level, diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 8ec91485b26..4faba9dc87b 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -49,7 +49,7 @@ module API runner = get_runner(params[:id]) authenticate_update_runner!(runner) - attrs = attributes_for_keys [:description, :active, :tag_list] + attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged] if runner.update(attrs) present runner, with: Entities::RunnerDetails, current_user: current_user else diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb new file mode 100644 index 00000000000..c49e2a21b82 --- /dev/null +++ b/lib/api/subscriptions.rb @@ -0,0 +1,60 @@ +module API + class Subscriptions < Grape::API + before { authenticate! } + + subscribable_types = { + 'merge_request' => proc { |id| user_project.merge_requests.find(id) }, + 'merge_requests' => proc { |id| user_project.merge_requests.find(id) }, + 'issues' => proc { |id| find_project_issue(id) }, + 'labels' => proc { |id| find_project_label(id) }, + } + + resource :projects do + subscribable_types.each do |type, finder| + type_singularized = type.singularize + type_id_str = :"#{type_singularized}_id" + entity_class = Entities.const_get(type_singularized.camelcase) + + # Subscribe to a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # POST /projects/:id/labels/:subscribable_id/subscription + # POST /projects/:id/issues/:subscribable_id/subscription + # POST /projects/:id/merge_requests/:subscribable_id/subscription + post ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if resource.subscribed?(current_user) + not_modified! + else + resource.subscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + + # Unsubscribe from a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # DELETE /projects/:id/labels/:subscribable_id/subscription + # DELETE /projects/:id/issues/:subscribable_id/subscription + # DELETE /projects/:id/merge_requests/:subscribable_id/subscription + delete ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if !resource.subscribed?(current_user) + not_modified! + else + resource.unsubscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index ea6fa2dc8a8..8a376d3c2a3 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -76,7 +76,7 @@ module API required_attributes! [:email, :password, :name, :username] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external] admin = attrs.delete(:admin) - confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) + confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i) user = User.build_user(attrs) user.admin = admin unless admin.nil? user.skip_confirmation! unless confirm diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 4962f5e53ce..660ca8c2923 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,5 +1,8 @@ module Backup class Manager + ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry] + FOLDERS_TO_BACKUP = %w[repositories db] + def pack # Make sure there is a connection ActiveRecord::Base.connection.reconnect! @@ -45,7 +48,7 @@ module Backup end connection = ::Fog::Storage.new(connection_settings) - directory = connection.directories.get(remote_directory) + directory = connection.directories.create(key: remote_directory) if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, @@ -147,7 +150,7 @@ module Backup end def skipped?(item) - settings[:skipped] && settings[:skipped].include?(item) + settings[:skipped] && settings[:skipped].include?(item) || disabled_features.include?(item) end private @@ -157,11 +160,17 @@ module Backup end def archives_to_backup - %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact + ARCHIVES_TO_BACKUP.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact end def folders_to_backup - %w{repositories db}.reject{ |name| skipped?(name) } + FOLDERS_TO_BACKUP.reject{ |name| skipped?(name) } + end + + def disabled_features + features = [] + features << 'registry' unless Gitlab.config.registry.enabled + features end def settings diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb new file mode 100644 index 00000000000..67fe0231087 --- /dev/null +++ b/lib/backup/registry.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Registry < Files + def initialize + super('registry', Settings.registry.path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index b8962379cb5..db95d7c908b 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -18,10 +18,6 @@ module Banzai @object_sym ||= object_name.to_sym end - def self.data_reference - @data_reference ||= "data-#{object_name.dasherize}" - end - def self.object_class_title @object_title ||= object_class.name.titleize end @@ -45,10 +41,6 @@ module Banzai end end - def self.referenced_by(node) - { object_sym => LazyReference.new(object_class, node.attr(data_reference)) } - end - def object_class self.class.object_class end @@ -236,7 +228,9 @@ module Banzai if cache.key?(key) cache[key] else - cache[key] = yield + value = yield + cache[key] = value if key.present? + value end end end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index b469ea0f626..bbb88c979cc 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # # This filter supports cross-project references. class CommitRangeReferenceFilter < AbstractReferenceFilter + self.reference_type = :commit_range + def self.object_class CommitRange end @@ -14,34 +16,18 @@ module Banzai end end - def self.referenced_by(node) - project = Project.find(node.attr("data-project")) rescue nil - return unless project - - id = node.attr("data-commit-range") - range = find_object(project, id) - - return unless range - - { commit_range: range } - end - def initialize(*args) super @commit_map = {} end - def self.find_object(project, id) + def find_object(project, id) range = CommitRange.new(id, project) range.valid_commits? ? range : nil end - def find_object(*args) - self.class.find_object(*args) - end - def url_for_object(range, project) h = Gitlab::Routing.url_helpers h.namespace_project_compare_url(project.namespace, project, diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index bd88207326c..2ce1816672b 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # # This filter supports cross-project references. class CommitReferenceFilter < AbstractReferenceFilter + self.reference_type = :commit + def self.object_class Commit end @@ -14,28 +16,12 @@ module Banzai end end - def self.referenced_by(node) - project = Project.find(node.attr("data-project")) rescue nil - return unless project - - id = node.attr("data-commit") - commit = find_object(project, id) - - return unless commit - - { commit: commit } - end - - def self.find_object(project, id) + def find_object(project, id) if project && project.valid_repo? project.commit(id) end end - def find_object(*args) - self.class.find_object(*args) - end - def url_for_object(commit, project) h = Gitlab::Routing.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 37344b90576..eaa702952cc 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # References are ignored if the project doesn't use an external issue # tracker. class ExternalIssueReferenceFilter < ReferenceFilter + self.reference_type = :external_issue + # Public: Find `JIRA-123` issue references in text # # ExternalIssueReferenceFilter.references_in(text) do |match, issue| @@ -21,18 +23,6 @@ module Banzai end end - def self.referenced_by(node) - project = Project.find(node.attr("data-project")) rescue nil - return unless project - - id = node.attr("data-external-issue") - external_issue = ExternalIssue.new(id, project) - - return unless external_issue - - { external_issue: external_issue } - end - def call # Early return if the project isn't using an external tracker return doc if project.nil? || default_issues_tracker? diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb new file mode 100644 index 00000000000..beb21b19ab3 --- /dev/null +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -0,0 +1,26 @@ +module Banzai + module Filter + class InlineDiffFilter < HTML::Pipeline::Filter + IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set + + def call + search_text_nodes(doc).each do |node| + next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) + + content = node.to_html + html_content = inline_diff_filter(content) + + next if content == html_content + + node.replace(html_content) + end + doc + end + + def inline_diff_filter(text) + html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '\1\2') + html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '\1\2') + end + end + end +end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 2732e0b5145..2496e704002 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -5,15 +5,12 @@ module Banzai # # This filter supports cross-project references. class IssueReferenceFilter < AbstractReferenceFilter + self.reference_type = :issue + def self.object_class Issue end - def self.user_can_see_reference?(user, node, context) - issue = Issue.find(node.attr('data-issue')) rescue nil - Ability.abilities.allowed?(user, :read_issue, issue) - end - def find_object(project, id) project.get_issue(id) end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8488a493b55..e4d3f87d0aa 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -2,6 +2,8 @@ module Banzai module Filter # HTML filter that replaces label references with links. class LabelReferenceFilter < AbstractReferenceFilter + self.reference_type = :label + def self.object_class Label end diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index cad38a51851..ac5216d9cfb 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -5,6 +5,8 @@ module Banzai # # This filter supports cross-project references. class MergeRequestReferenceFilter < AbstractReferenceFilter + self.reference_type = :merge_request + def self.object_class MergeRequest end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 4cb82178024..ca686c87d97 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -2,6 +2,8 @@ module Banzai module Filter # HTML filter that replaces milestone references with links. class MilestoneReferenceFilter < AbstractReferenceFilter + self.reference_type = :milestone + def self.object_class Milestone end @@ -10,11 +12,53 @@ module Banzai project.milestones.find_by(iid: id) end - def url_for_object(issue, project) + def references_in(text, pattern = Milestone.reference_pattern) + # We'll handle here the references that follow the `reference_pattern`. + # Other patterns (for example, the link pattern) are handled by the + # default implementation. + return super(text, pattern) if pattern != Milestone.reference_pattern + + text.gsub(pattern) do |match| + milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name]) + + if milestone + yield match, milestone.iid, $~[:project], $~ + else + match + end + end + end + + def find_milestone(project_ref, milestone_id, milestone_name) + project = project_from_ref(project_ref) + return unless project + + milestone_params = milestone_params(milestone_id, milestone_name) + project.milestones.find_by(milestone_params) + end + + def milestone_params(iid, name) + if name + { name: name.tr('"', '') } + else + { iid: iid.to_i } + end + end + + def url_for_object(milestone, project) h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, only_path: context[:only_path]) end + + def object_link_text(object, matches) + if context[:project] == object.project + super + else + "#{escape_once(super)} in #{escape_once(object.project.path_with_namespace)}". + html_safe + end + end end end end diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index e589b5df6ec..c753a84a20d 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -7,8 +7,11 @@ module Banzai # class RedactorFilter < HTML::Pipeline::Filter def call - Querying.css(doc, 'a.gfm').each do |node| - unless user_can_see_reference?(node) + nodes = Querying.css(doc, 'a.gfm[data-reference-type]') + visible = nodes_visible_to_user(nodes) + + nodes.each do |node| + unless visible.include?(node) # The reference should be replaced by the original text, # which is not always the same as the rendered text. text = node.attr('data-original') || node.text @@ -21,20 +24,30 @@ module Banzai private - def user_can_see_reference?(node) - if node.has_attribute?('data-reference-filter') - reference_type = node.attr('data-reference-filter') - reference_filter = Banzai::Filter.const_get(reference_type) + def nodes_visible_to_user(nodes) + per_type = Hash.new { |h, k| h[k] = [] } + visible = Set.new + + nodes.each do |node| + per_type[node.attr('data-reference-type')] << node + end + + per_type.each do |type, nodes| + parser = Banzai::ReferenceParser[type].new(project, current_user) - reference_filter.user_can_see_reference?(current_user, node, context) - else - true + visible.merge(parser.nodes_visible_to_user(current_user, nodes)) end + + visible end def current_user context[:current_user] end + + def project + context[:project] + end end end end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 31386cf851c..41ae0e1f9cc 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -8,24 +8,8 @@ module Banzai # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. class ReferenceFilter < HTML::Pipeline::Filter - def self.user_can_see_reference?(user, node, context) - if node.has_attribute?('data-project') - project_id = node.attr('data-project').to_i - return true if project_id == context[:project].try(:id) - - project = Project.find(project_id) rescue nil - Ability.abilities.allowed?(user, :read_project, project) - else - true - end - end - - def self.user_can_reference?(user, node, context) - true - end - - def self.referenced_by(node) - raise NotImplementedError, "#{self} does not implement #{__method__}" + class << self + attr_accessor :reference_type end # Returns a data attribute String to attach to a reference link @@ -43,7 +27,9 @@ module Banzai # # Returns a String def data_attribute(attributes = {}) - attributes[:reference_filter] = self.class.name.demodulize + attributes = attributes.reject { |_, v| v.nil? } + + attributes[:reference_type] = self.class.reference_type attributes.delete(:original) if context[:no_original_data] attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") end diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb deleted file mode 100644 index 96fdb06304e..00000000000 --- a/lib/banzai/filter/reference_gatherer_filter.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Banzai - module Filter - # HTML filter that gathers all referenced records that the current user has - # permission to view. - # - # Expected to be run in its own post-processing pipeline. - # - class ReferenceGathererFilter < HTML::Pipeline::Filter - def initialize(*) - super - - result[:references] ||= Hash.new { |hash, type| hash[type] = [] } - end - - def call - Querying.css(doc, 'a.gfm').each do |node| - gather_references(node) - end - - load_lazy_references unless ReferenceExtractor.lazy? - - doc - end - - private - - def gather_references(node) - return unless node.has_attribute?('data-reference-filter') - - reference_type = node.attr('data-reference-filter') - reference_filter = Banzai::Filter.const_get(reference_type) - - return if context[:reference_filter] && reference_filter != context[:reference_filter] - - return if author && !reference_filter.user_can_reference?(author, node, context) - - return unless reference_filter.user_can_see_reference?(current_user, node, context) - - references = reference_filter.referenced_by(node) - return unless references - - references.each do |type, values| - Array.wrap(values).each do |value| - result[:references][type] << value - end - end - end - - def load_lazy_references - refs = result[:references] - refs.each do |type, values| - refs[type] = ReferenceExtractor.lazily(values) - end - end - - def current_user - context[:current_user] - end - - def author - context[:author] - end - end - end -end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 42dbab9d27e..ca80aac5a08 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -63,7 +63,7 @@ module Banzai begin uri = Addressable::URI.parse(node['href']) - uri.scheme.strip! if uri.scheme + uri.scheme = uri.scheme.strip.downcase if uri.scheme node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) rescue Addressable::URI::InvalidURIError diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index d507eb5ebe1..212a0bbf2a0 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -5,6 +5,8 @@ module Banzai # # This filter supports cross-project references. class SnippetReferenceFilter < AbstractReferenceFilter + self.reference_type = :snippet + def self.object_class Snippet end diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb index 7edfe5ade2d..c0f503c9af3 100644 --- a/lib/banzai/filter/upload_link_filter.rb +++ b/lib/banzai/filter/upload_link_filter.rb @@ -8,6 +8,8 @@ module Banzai # class UploadLinkFilter < HTML::Pipeline::Filter def call + return doc unless project + doc.search('a').each do |el| process_link_attr el.attribute('href') end @@ -31,7 +33,11 @@ module Banzai end def build_url(uri) - File.join(Gitlab.config.gitlab.url, context[:project].path_with_namespace, uri) + File.join(Gitlab.config.gitlab.url, project.path_with_namespace, uri) + end + + def project + context[:project] end # Ensure that a :project key exists in context diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index eea3af842b6..331d8007257 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # # A special `@all` reference is also supported. class UserReferenceFilter < ReferenceFilter + self.reference_type = :user + # Public: Find `@user` user references in text # # UserReferenceFilter.references_in(text) do |match, username| @@ -21,43 +23,6 @@ module Banzai end end - def self.referenced_by(node) - if node.has_attribute?('data-group') - group = Group.find(node.attr('data-group')) rescue nil - return unless group - - { user: group.users } - elsif node.has_attribute?('data-user') - { user: LazyReference.new(User, node.attr('data-user')) } - elsif node.has_attribute?('data-project') - project = Project.find(node.attr('data-project')) rescue nil - return unless project - - { user: project.team.members.flatten } - end - end - - def self.user_can_see_reference?(user, node, context) - if node.has_attribute?('data-group') - group = Group.find(node.attr('data-group')) rescue nil - Ability.abilities.allowed?(user, :read_group, group) - else - super - end - end - - def self.user_can_reference?(user, node, context) - # Only team members can reference `@all` - if node.has_attribute?('data-project') - project = Project.find(node.attr('data-project')) rescue nil - return false unless project - - user && project.team.member?(user) - else - super - end - end - def call return doc if project.nil? @@ -114,9 +79,12 @@ module Banzai def link_to_all(link_text: nil) project = context[:project] + author = context[:author] + url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) - data = data_attribute(project: project.id) + + data = data_attribute(project: project.id, author: author.try(:id)) text = link_text || User.reference_prefix + 'all' link_tag(url, data, text) diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 06d10c98501..7dc771afd71 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -25,7 +25,7 @@ module Banzai end def process_link_attr(html_attr) - return if html_attr.blank? || file_reference?(html_attr) + return if html_attr.blank? || file_reference?(html_attr) || hierarchical_link?(html_attr) uri = URI(html_attr.value) if uri.relative? && uri.path.present? @@ -40,12 +40,17 @@ module Banzai uri end + def project_wiki + context[:project_wiki] + end + def file_reference?(html_attr) !File.extname(html_attr.value).blank? end - def project_wiki - context[:project_wiki] + # Of the form `./link`, `../link`, or similar + def hierarchical_link?(html_attr) + html_attr.value[0] == '.' end def project_wiki_base_path diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb deleted file mode 100644 index 1095b4debc7..00000000000 --- a/lib/banzai/lazy_reference.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Banzai - class LazyReference - def self.load(refs) - lazy_references, values = refs.partition { |ref| ref.is_a?(self) } - - lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs| - ids = refs.flat_map(&:ids) - klass.where(id: ids) - end - - values + lazy_values - end - - attr_reader :klass, :ids - - def initialize(klass, ids) - @klass = klass - @ids = Array.wrap(ids).map(&:to_i) - end - - def load - self.klass.where(id: self.ids) - end - end -end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index ed3cfd6b023..b27ecf3c923 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -23,7 +23,8 @@ module Banzai Filter::LabelReferenceFilter, Filter::MilestoneReferenceFilter, - Filter::TaskListFilter + Filter::TaskListFilter, + Filter::InlineDiffFilter ] end diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb deleted file mode 100644 index 919998380e4..00000000000 --- a/lib/banzai/pipeline/reference_extraction_pipeline.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Banzai - module Pipeline - class ReferenceExtractionPipeline < BasePipeline - def self.filters - FilterArray[ - Filter::ReferenceGathererFilter - ] - end - end - end -end diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb index f4079538ec5..bf366962aef 100644 --- a/lib/banzai/reference_extractor.rb +++ b/lib/banzai/reference_extractor.rb @@ -1,28 +1,6 @@ module Banzai # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - class << self - LAZY_KEY = :banzai_reference_extractor_lazy - - def lazy? - Thread.current[LAZY_KEY] - end - - def lazily(values = nil, &block) - return (values || block.call).uniq if lazy? - - begin - Thread.current[LAZY_KEY] = true - - values ||= block.call - - Banzai::LazyReference.load(values.uniq).uniq - ensure - Thread.current[LAZY_KEY] = false - end - end - end - def initialize @texts = [] end @@ -31,23 +9,21 @@ module Banzai @texts << Renderer.render(text, context) end - def references(type, context = {}) - filter = Banzai::Filter["#{type}_reference"] + def references(type, project, current_user = nil) + processor = Banzai::ReferenceParser[type]. + new(project, current_user) + + processor.process(html_documents) + end - context.merge!( - pipeline: :reference_extraction, + private - # ReferenceGathererFilter - reference_filter: filter - ) + def html_documents + # This ensures that we don't memoize anything until we have a number of + # text blobs to parse. + return [] if @texts.empty? - self.class.lazily do - @texts.flat_map do |html| - text_context = context.dup - result = Renderer.render_result(html, text_context) - result[:references][type] - end.uniq - end + @html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) } end end end diff --git a/lib/banzai/reference_parser.rb b/lib/banzai/reference_parser.rb new file mode 100644 index 00000000000..557bec4316e --- /dev/null +++ b/lib/banzai/reference_parser.rb @@ -0,0 +1,14 @@ +module Banzai + module ReferenceParser + # Returns the reference parser class for the given type + # + # Example: + # + # Banzai::ReferenceParser['issue'] + # + # This would return the `Banzai::ReferenceParser::IssueParser` class. + def self.[](name) + const_get("#{name.to_s.camelize}Parser") + end + end +end diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb new file mode 100644 index 00000000000..3d7b9c4a024 --- /dev/null +++ b/lib/banzai/reference_parser/base_parser.rb @@ -0,0 +1,204 @@ +module Banzai + module ReferenceParser + # Base class for reference parsing classes. + # + # Each parser should also specify its reference type by calling + # `self.reference_type = ...` in the body of the class. The value of this + # method should be a symbol such as `:issue` or `:merge_request`. For + # example: + # + # class IssueParser < BaseParser + # self.reference_type = :issue + # end + # + # The reference type is used to determine what nodes to pass to the + # `referenced_by` method. + # + # Parser classes should either implement the instance method + # `references_relation` or overwrite `referenced_by`. The + # `references_relation` method is supposed to return an + # ActiveRecord::Relation used as a base relation for retrieving the objects + # referenced in a set of HTML nodes. + # + # Each class can implement two additional methods: + # + # * `nodes_user_can_reference`: returns an Array of nodes the given user can + # refer to. + # * `nodes_visible_to_user`: returns an Array of nodes that are visible to + # the given user. + # + # You only need to overwrite these methods if you want to tweak who can see + # which references. For example, the IssueParser class defines its own + # `nodes_visible_to_user` method so it can ensure users can only see issues + # they have access to. + class BaseParser + class << self + attr_accessor :reference_type + end + + # Returns the attribute name containing the value for every object to be + # parsed by the current parser. + # + # For example, for a parser class that returns "Animal" objects this + # attribute would be "data-animal". + def self.data_attribute + @data_attribute ||= "data-#{reference_type.to_s.dasherize}" + end + + def initialize(project = nil, current_user = nil) + @project = project + @current_user = current_user + end + + # Returns all the nodes containing references that the user can refer to. + def nodes_user_can_reference(user, nodes) + nodes + end + + # Returns all the nodes that are visible to the given user. + def nodes_visible_to_user(user, nodes) + projects = lazy { projects_for_nodes(nodes) } + project_attr = 'data-project' + + nodes.select do |node| + if node.has_attribute?(project_attr) + node_id = node.attr(project_attr).to_i + + if project && project.id == node_id + true + else + can?(user, :read_project, projects[node_id]) + end + else + true + end + end + end + + # Returns an Array of objects referenced by any of the given HTML nodes. + def referenced_by(nodes) + ids = unique_attribute_values(nodes, self.class.data_attribute) + + references_relation.where(id: ids) + end + + # Returns the ActiveRecord::Relation to use for querying references in the + # DB. + def references_relation + raise NotImplementedError, + "#{self.class} does not implement #{__method__}" + end + + # Returns a Hash containing attribute values per project ID. + # + # The returned Hash uses the following format: + # + # { project id => [value1, value2, ...] } + # + # nodes - An Array of HTML nodes to process. + # attribute - The name of the attribute (as a String) for which to gather + # values. + # + # Returns a Hash. + def gather_attributes_per_project(nodes, attribute) + per_project = Hash.new { |hash, key| hash[key] = Set.new } + + nodes.each do |node| + project_id = node.attr('data-project').to_i + id = node.attr(attribute) + + per_project[project_id] << id if id + end + + per_project + end + + # Returns a Hash containing objects for an attribute grouped per their + # IDs. + # + # The returned Hash uses the following format: + # + # { id value => row } + # + # nodes - An Array of HTML nodes to process. + # + # collection - The model or ActiveRecord relation to use for retrieving + # rows from the database. + # + # attribute - The name of the attribute containing the primary key values + # for every row. + # + # Returns a Hash. + def grouped_objects_for_nodes(nodes, collection, attribute) + return {} if nodes.empty? + + ids = unique_attribute_values(nodes, attribute) + + collection.where(id: ids).each_with_object({}) do |row, hash| + hash[row.id] = row + end + end + + # Returns an Array containing all unique values of an attribute of the + # given nodes. + def unique_attribute_values(nodes, attribute) + values = Set.new + + nodes.each do |node| + if node.has_attribute?(attribute) + values << node.attr(attribute) + end + end + + values.to_a + end + + # Processes the list of HTML documents and returns an Array containing all + # the references. + def process(documents) + type = self.class.reference_type + + nodes = documents.flat_map do |document| + Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a + end + + gather_references(nodes) + end + + # Gathers the references for the given HTML nodes. + def gather_references(nodes) + nodes = nodes_user_can_reference(current_user, nodes) + nodes = nodes_visible_to_user(current_user, nodes) + + referenced_by(nodes) + end + + # Returns a Hash containing the projects for a given list of HTML nodes. + # + # The returned Hash uses the following format: + # + # { project ID => project } + # + def projects_for_nodes(nodes) + @projects_for_nodes ||= + grouped_objects_for_nodes(nodes, Project, 'data-project') + end + + def can?(user, permission, subject) + Ability.abilities.allowed?(user, permission, subject) + end + + def find_projects_for_hash_keys(hash) + Project.where(id: hash.keys) + end + + private + + attr_reader :current_user, :project + + def lazy(&block) + Gitlab::Lazy.new(&block) + end + end + end +end diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb new file mode 100644 index 00000000000..0fee9d267de --- /dev/null +++ b/lib/banzai/reference_parser/commit_parser.rb @@ -0,0 +1,34 @@ +module Banzai + module ReferenceParser + class CommitParser < BaseParser + self.reference_type = :commit + + def referenced_by(nodes) + commit_ids = commit_ids_per_project(nodes) + projects = find_projects_for_hash_keys(commit_ids) + + projects.flat_map do |project| + find_commits(project, commit_ids[project.id]) + end + end + + def commit_ids_per_project(nodes) + gather_attributes_per_project(nodes, self.class.data_attribute) + end + + def find_commits(project, ids) + commits = [] + + return commits unless project.valid_repo? + + ids.each do |id| + commit = project.commit(id) + + commits << commit if commit + end + + commits + end + end + end +end diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb new file mode 100644 index 00000000000..69d01f8db15 --- /dev/null +++ b/lib/banzai/reference_parser/commit_range_parser.rb @@ -0,0 +1,38 @@ +module Banzai + module ReferenceParser + class CommitRangeParser < BaseParser + self.reference_type = :commit_range + + def referenced_by(nodes) + range_ids = commit_range_ids_per_project(nodes) + projects = find_projects_for_hash_keys(range_ids) + + projects.flat_map do |project| + find_ranges(project, range_ids[project.id]) + end + end + + def commit_range_ids_per_project(nodes) + gather_attributes_per_project(nodes, self.class.data_attribute) + end + + def find_ranges(project, range_ids) + ranges = [] + + range_ids.each do |id| + range = find_object(project, id) + + ranges << range if range + end + + ranges + end + + def find_object(project, id) + range = CommitRange.new(id, project) + + range.valid_commits? ? range : nil + end + end + end +end diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb new file mode 100644 index 00000000000..a1264db2111 --- /dev/null +++ b/lib/banzai/reference_parser/external_issue_parser.rb @@ -0,0 +1,25 @@ +module Banzai + module ReferenceParser + class ExternalIssueParser < BaseParser + self.reference_type = :external_issue + + def referenced_by(nodes) + issue_ids = issue_ids_per_project(nodes) + projects = find_projects_for_hash_keys(issue_ids) + issues = [] + + projects.each do |project| + issue_ids[project.id].each do |id| + issues << ExternalIssue.new(id, project) + end + end + + issues + end + + def issue_ids_per_project(nodes) + gather_attributes_per_project(nodes, self.class.data_attribute) + end + end + end +end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb new file mode 100644 index 00000000000..24076e3d9ec --- /dev/null +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -0,0 +1,40 @@ +module Banzai + module ReferenceParser + class IssueParser < BaseParser + self.reference_type = :issue + + def nodes_visible_to_user(user, nodes) + # It is not possible to check access rights for external issue trackers + return nodes if project && project.external_issue_tracker + + issues = issues_for_nodes(nodes) + + nodes.select do |node| + issue = issue_for_node(issues, node) + + issue ? can?(user, :read_issue, issue) : false + end + end + + def referenced_by(nodes) + issues = issues_for_nodes(nodes) + + nodes.map { |node| issue_for_node(issues, node) }.uniq + end + + def issues_for_nodes(nodes) + @issues_for_nodes ||= grouped_objects_for_nodes( + nodes, + Issue.all.includes(:author, :assignee, :project), + self.class.data_attribute + ) + end + + private + + def issue_for_node(issues, node) + issues[node.attr(self.class.data_attribute).to_i] + end + end + end +end diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb new file mode 100644 index 00000000000..e5d1eb11d7f --- /dev/null +++ b/lib/banzai/reference_parser/label_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class LabelParser < BaseParser + self.reference_type = :label + + def references_relation + Label + end + end + end +end diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb new file mode 100644 index 00000000000..c9a9ca79c09 --- /dev/null +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class MergeRequestParser < BaseParser + self.reference_type = :merge_request + + def references_relation + MergeRequest.includes(:author, :assignee, :target_project) + end + end + end +end diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb new file mode 100644 index 00000000000..a000ac61e5c --- /dev/null +++ b/lib/banzai/reference_parser/milestone_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class MilestoneParser < BaseParser + self.reference_type = :milestone + + def references_relation + Milestone + end + end + end +end diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb new file mode 100644 index 00000000000..fa71b3c952a --- /dev/null +++ b/lib/banzai/reference_parser/snippet_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class SnippetParser < BaseParser + self.reference_type = :snippet + + def references_relation + Snippet + end + end + end +end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb new file mode 100644 index 00000000000..a12b0d19560 --- /dev/null +++ b/lib/banzai/reference_parser/user_parser.rb @@ -0,0 +1,92 @@ +module Banzai + module ReferenceParser + class UserParser < BaseParser + self.reference_type = :user + + def referenced_by(nodes) + group_ids = [] + user_ids = [] + project_ids = [] + + nodes.each do |node| + if node.has_attribute?('data-group') + group_ids << node.attr('data-group').to_i + elsif node.has_attribute?(self.class.data_attribute) + user_ids << node.attr(self.class.data_attribute).to_i + elsif node.has_attribute?('data-project') + project_ids << node.attr('data-project').to_i + end + end + + find_users_for_groups(group_ids) | find_users(user_ids) | + find_users_for_projects(project_ids) + end + + def nodes_visible_to_user(user, nodes) + group_attr = 'data-group' + groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) } + visible = [] + remaining = [] + + nodes.each do |node| + if node.has_attribute?(group_attr) + node_group = groups[node.attr(group_attr).to_i] + + if node_group && + can?(user, :read_group, node_group) + visible << node + end + # Remaining nodes will be processed by the parent class' + # implementation of this method. + else + remaining << node + end + end + + visible + super(current_user, remaining) + end + + def nodes_user_can_reference(current_user, nodes) + project_attr = 'data-project' + author_attr = 'data-author' + + projects = lazy { projects_for_nodes(nodes) } + users = lazy { grouped_objects_for_nodes(nodes, User, author_attr) } + + nodes.select do |node| + project_id = node.attr(project_attr) + user_id = node.attr(author_attr) + + if project && project_id && project.id == project_id.to_i + true + elsif project_id && user_id + project = projects[project_id.to_i] + user = users[user_id.to_i] + + project && user ? project.team.member?(user) : false + else + true + end + end + end + + def find_users(ids) + return [] if ids.empty? + + User.where(id: ids).to_a + end + + def find_users_for_groups(ids) + return [] if ids.empty? + + User.joins(:group_members).where(members: { source_id: ids }).to_a + end + + def find_users_for_projects(ids) + return [] if ids.empty? + + Project.where(id: ids).flat_map { |p| p.team.members.to_a } + end + end + end +end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index ac6d667cf8d..229050151d3 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -23,8 +23,8 @@ module Ci cross: 0x10, } - def self.convert(ansi) - Converter.new().convert(ansi) + def self.convert(ansi, state = nil) + Converter.new.convert(ansi, state) end class Converter @@ -84,22 +84,38 @@ module Ci def on_107(s) set_bg_color(7, 'l') end def on_109(s) set_bg_color(9, 'l') end - def convert(ansi) - @out = "" - @n_open_tags = 0 - reset() + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask + + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask] + + def convert(raw, new_state) + reset_state + restore_state(raw, new_state) if new_state.present? + + start = @offset + ansi = raw[@offset..-1] + + open_new_tag - s = StringScanner.new(ansi.gsub("<", "<")) - while(!s.eos?) + s = StringScanner.new(ansi) + until s.eos? if s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) + elsif s.scan(/\e(([@-_])(.*?)?)?$/) + break + elsif s.scan(/' else @out << s.scan(/./m) end + @offset += s.matched_size end close_open_tags() - @out + + { state: state, html: @out, text: ansi[0, @offset - start], append: start > 0 } end def handle_sequence(s) @@ -121,6 +137,20 @@ module Ci evaluate_command_stack(commands) + open_new_tag + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.send("on_#{command}", stack) + end + + evaluate_command_stack(stack) + end + + def open_new_tag css_classes = [] unless @fg_color.nil? @@ -138,20 +168,8 @@ module Ci css_classes << "term-#{css_class}" if @style_mask & flag != 0 end - open_new_tag(css_classes) if css_classes.length > 0 - end + return if css_classes.empty? - def evaluate_command_stack(stack) - return unless command = stack.shift() - - if self.respond_to?("on_#{command}", true) - self.send("on_#{command}", stack) - end - - evaluate_command_stack(stack) - end - - def open_new_tag(css_classes) @out << %{} @n_open_tags += 1 end @@ -163,6 +181,31 @@ module Ci end end + def reset_state + @offset = 0 + @n_open_tags = 0 + @out = '' + reset + end + + def state + state = STATE_PARAMS.inject({}) do |h, param| + h[param] = send(param) + h + end + Base64.urlsafe_encode64(state.to_json) + end + + def restore_state(raw, new_state) + state = Base64.urlsafe_decode64(new_state) + state = JSON.parse(state, symbolize_names: true) + return if state[:offset].to_i > raw.length + + STATE_PARAMS.each do |param| + send("#{param}=".to_sym, state[param]) + end + end + def reset @fg_color = nil @bg_color = nil diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 353c4ddebf8..17bb99a2ae5 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -1,9 +1,7 @@ -Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} - module Ci module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do @@ -31,9 +29,9 @@ module Ci helpers ::API::Helpers helpers Gitlab::CurrentSettings - mount Builds - mount Runners - mount Triggers + mount ::Ci::API::Builds + mount ::Ci::API::Runners + mount ::Ci::API::Triggers end end end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 192b1d18a51..0c41f22c7c5 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -28,20 +28,20 @@ module Ci post "register" do required_attributes! [:token] + attributes = { description: params[:description], + tag_list: params[:tag_list] } + + unless params[:run_untagged].nil? + attributes[:run_untagged] = params[:run_untagged] + end + runner = if runner_registration_token_valid? # Create shared runner. Requires admin access - Ci::Runner.create( - description: params[:description], - tag_list: params[:tag_list], - is_shared: true - ) + Ci::Runner.create(attributes.merge(is_shared: true)) elsif project = Project.find_by(runners_token: params[:token]) # Create a specific runner for project. - project.runners.create( - description: params[:description], - tag_list: params[:tag_list] - ) + project.runners.create(attributes) end return forbidden! unless runner diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index d53bdcbd0f2..e1636636934 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -64,7 +64,8 @@ module Ci commits.each do |commit| @labels << commit.short_sha - @build_times << (commit.duration / 60) + duration = commit.duration || 0 + @build_times << (duration / 60) end end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 504d3df9d34..026a5ac97ca 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -1,6 +1,6 @@ module Ci class GitlabCiYamlProcessor - class ValidationError < StandardError;end + class ValidationError < StandardError; end DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGE = 'test' @@ -265,7 +265,7 @@ module Ci end def validate_job_dependencies!(name, job) - if !validate_array_of_strings(job[:dependencies]) + unless validate_array_of_strings(job[:dependencies]) raise ValidationError, "#{name} job: dependencies parameter should be an array of strings" end diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb new file mode 100644 index 00000000000..4e20dc4f875 --- /dev/null +++ b/lib/container_registry/blob.rb @@ -0,0 +1,48 @@ +module ContainerRegistry + class Blob + attr_reader :repository, :config + + delegate :registry, :client, to: :repository + + def initialize(repository, config) + @repository = repository + @config = config || {} + end + + def valid? + digest.present? + end + + def path + "#{repository.path}@#{digest}" + end + + def digest + config['digest'] + end + + def type + config['mediaType'] + end + + def size + config['size'] + end + + def revision + digest.split(':')[1] + end + + def short_revision + revision[0..8] + end + + def delete + client.delete_blob(repository.name, digest) + end + + def data + @data ||= client.blob(repository.name, digest, type) + end + end +end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb new file mode 100644 index 00000000000..4d726692f45 --- /dev/null +++ b/lib/container_registry/client.rb @@ -0,0 +1,61 @@ +require 'faraday' +require 'faraday_middleware' + +module ContainerRegistry + class Client + attr_accessor :uri + + MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json' + + def initialize(base_uri, options = {}) + @base_uri = base_uri + @faraday = Faraday.new(@base_uri) do |conn| + initialize_connection(conn, options) + end + end + + def repository_tags(name) + @faraday.get("/v2/#{name}/tags/list").body + end + + def repository_manifest(name, reference) + @faraday.get("/v2/#{name}/manifests/#{reference}").body + end + + def repository_tag_digest(name, reference) + response = @faraday.head("/v2/#{name}/manifests/#{reference}") + response.headers['docker-content-digest'] if response.success? + end + + def delete_repository_tag(name, reference) + @faraday.delete("/v2/#{name}/manifests/#{reference}").success? + end + + def blob(name, digest, type = nil) + headers = {} + headers['Accept'] = type if type + @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body + end + + def delete_blob(name, digest) + @faraday.delete("/v2/#{name}/blobs/#{digest}").success? + end + + private + + def initialize_connection(conn, options) + conn.request :json + conn.headers['Accept'] = MANIFEST_VERSION + + conn.response :json, content_type: /\bjson$/ + + if options[:user] && options[:password] + conn.request(:basic_auth, options[:user].to_s, options[:password].to_s) + elsif options[:token] + conn.request(:authorization, :bearer, options[:token].to_s) + end + + conn.adapter :net_http + end + end +end diff --git a/lib/container_registry/config.rb b/lib/container_registry/config.rb new file mode 100644 index 00000000000..589f9f4380a --- /dev/null +++ b/lib/container_registry/config.rb @@ -0,0 +1,16 @@ +module ContainerRegistry + class Config + attr_reader :tag, :blob, :data + + def initialize(tag, blob) + @tag, @blob = tag, blob + @data = JSON.parse(blob.data) + end + + def [](key) + return unless data + + data[key] + end + end +end diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb new file mode 100644 index 00000000000..0e634f6b6ef --- /dev/null +++ b/lib/container_registry/registry.rb @@ -0,0 +1,21 @@ +module ContainerRegistry + class Registry + attr_reader :uri, :client, :path + + def initialize(uri, options = {}) + @uri = uri + @path = options[:path] || default_path + @client = ContainerRegistry::Client.new(uri, options) + end + + def repository(name) + ContainerRegistry::Repository.new(self, name) + end + + private + + def default_path + @uri.sub(/^https?:\/\//, '') + end + end +end diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb new file mode 100644 index 00000000000..0e4a7cb3cc9 --- /dev/null +++ b/lib/container_registry/repository.rb @@ -0,0 +1,48 @@ +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/lib/container_registry/tag.rb b/lib/container_registry/tag.rb new file mode 100644 index 00000000000..43f8d6dc8c2 --- /dev/null +++ b/lib/container_registry/tag.rb @@ -0,0 +1,77 @@ +module ContainerRegistry + class Tag + attr_reader :repository, :name + + delegate :registry, :client, to: :repository + + def initialize(repository, name) + @repository, @name = repository, name + end + + def valid? + manifest.present? + end + + def manifest + return @manifest if defined?(@manifest) + + @manifest = client.repository_manifest(repository.name, name) + end + + def path + "#{repository.path}:#{name}" + end + + def [](key) + return unless manifest + + manifest[key] + end + + def digest + return @digest if defined?(@digest) + + @digest = client.repository_tag_digest(repository.name, name) + end + + def config_blob + return @config_blob if defined?(@config_blob) + return unless manifest && manifest['config'] + + @config_blob = repository.blob(manifest['config']) + end + + def config + return unless config_blob + + @config ||= ContainerRegistry::Config.new(self, config_blob) + end + + def created_at + return unless config + + @created_at ||= DateTime.rfc3339(config['created']) + end + + def layers + return @layers if defined?(@layers) + return unless manifest + + @layers = manifest['layers'].map do |layer| + repository.blob(layer) + end + end + + def total_size + return unless layers + + layers.map(&:size).sum + end + + def delete + return unless digest + + client.delete_repository_tag(repository.name, digest) + end + end +end diff --git a/lib/event_filter.rb b/lib/event_filter.rb index f15b2cfd231..668d2fa41b3 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -27,7 +27,7 @@ class EventFilter @params = if params params.dup else - []#EventFilter.default_filter + [] # EventFilter.default_filter end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 7479e729db1..37f4c34054f 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,4 +1,4 @@ -require 'gitlab/git' +require_dependency 'gitlab/git' module Gitlab def self.com? diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 132f9cd1966..3e3986d6382 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -180,7 +180,7 @@ module Gitlab # exists?('gitlab/cookies.git') # def exists?(dir_name) - File.exists?(full_path(dir_name)) + File.exist?(full_path(dir_name)) end protected diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 9b83292ef33..8d1ad62fae0 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -121,7 +121,7 @@ module Gitlab def get(url) response = api.get(url) - raise Unauthorized if (400..499).include?(response.code.to_i) + raise Unauthorized if (400..499).cover?(response.code.to_i) response end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 941f818b847..b90ef0b0fba 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo["name"], path: repo["slug"], @@ -21,11 +21,8 @@ module Gitlab import_type: "bitbucket", import_source: "#{repo["owner"]}/#{repo["slug"]}", import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", + import_data: { credentials: { bb_session: session_data } } ).execute - - project.create_or_update_import_data(credentials: { bb_session: session_data }) - - project end end end diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index f2020c82d40..cd2e83b4c27 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -56,7 +56,7 @@ module Gitlab child_pattern = '[^/]*/?$' unless @opts[:recursive] match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/ - until gz.eof? do + until gz.eof? begin path = read_string(gz).force_encoding('UTF-8') meta = read_string(gz).force_encoding('UTF-8') diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 85583dce9ee..9dc2602867e 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -19,7 +19,7 @@ module Gitlab select('date(created_at) as date, count(id) as total_amount'). map(&:attributes) - dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a + dates = (1.year.ago.to_date..Date.today).to_a dates.each do |date| date_id = date.to_time.to_i.to_s diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index f44d1b3a44e..92c7e8b9d88 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -1,18 +1,22 @@ module Gitlab module CurrentSettings def current_application_settings - key = :current_application_settings - - RequestStore.store[key] ||= begin - settings = nil + if RequestStore.active? + RequestStore.fetch(:current_application_settings) { ensure_application_settings! } + else + ensure_application_settings! + end + end - if connect_to_db? - settings = ::ApplicationSetting.current - settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration? - end + def ensure_application_settings! + settings = ::ApplicationSetting.cached - settings || fake_application_settings + if !settings && connect_to_db? + settings = ::ApplicationSetting.current + settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration? end + + settings || fake_application_settings end def fake_application_settings @@ -36,6 +40,7 @@ module Gitlab two_factor_grace_period: 48, akismet_enabled: false, repository_checks_enabled: true, + container_registry_token_expire_delay: 5, ) end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 6f9da69983a..42bec913a45 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -5,11 +5,11 @@ module Gitlab end def self.mysql? - adapter_name.downcase == 'mysql2' + adapter_name.casecmp('mysql2').zero? end def self.postgresql? - adapter_name.downcase == 'postgresql' + adapter_name.casecmp('postgresql').zero? end def self.version diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb new file mode 100644 index 00000000000..fd14234c558 --- /dev/null +++ b/lib/gitlab/database/migration_helpers.rb @@ -0,0 +1,142 @@ +module Gitlab + module Database + module MigrationHelpers + # Creates a new index, concurrently when supported + # + # On PostgreSQL this method creates an index concurrently, on MySQL this + # creates a regular index. + # + # Example: + # + # add_concurrent_index :users, :some_column + # + # See Rails' `add_index` for more info on the available arguments. + def add_concurrent_index(*args) + if transaction_open? + raise 'add_concurrent_index can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + if Database.postgresql? + args << { algorithm: :concurrently } + end + + add_index(*args) + end + + # Updates the value of a column in batches. + # + # This method updates the table in batches of 5% of the total row count. + # Any data inserted while running this method (or after it has finished + # running) is _not_ updated automatically. + # + # This method _only_ updates rows where the column's value is set to NULL. + # + # table - The name of the table. + # column - The name of the column to update. + # value - The value for the column. + def update_column_in_batches(table, column, value) + quoted_table = quote_table_name(table) + quoted_column = quote_column_name(column) + + ## + # Workaround for #17711 + # + # It looks like for MySQL `ActiveRecord::Base.conntection.quote(true)` + # returns correct value (1), but `ActiveRecord::Migration.new.quote` + # returns incorrect value ('true'), which causes migrations to fail. + # + quoted_value = connection.quote(value) + processed = 0 + + total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}"). + to_hash. + first['count']. + to_i + + # Update in batches of 5% + batch_size = ((total / 100.0) * 5.0).ceil + + while processed < total + start_row = exec_query(%Q{ + SELECT id + FROM #{quoted_table} + ORDER BY id ASC + LIMIT 1 OFFSET #{processed} + }).to_hash.first + + stop_row = exec_query(%Q{ + SELECT id + FROM #{quoted_table} + ORDER BY id ASC + LIMIT 1 OFFSET #{processed + batch_size} + }).to_hash.first + + query = %Q{ + UPDATE #{quoted_table} + SET #{quoted_column} = #{quoted_value} + WHERE id >= #{start_row['id']} + } + + if stop_row + query += " AND id < #{stop_row['id']}" + end + + execute(query) + + processed += batch_size + end + end + + # Adds a column with a default value without locking an entire table. + # + # This method runs the following steps: + # + # 1. Add the column with a default value of NULL. + # 2. Update all existing rows in batches. + # 3. Change the default value of the column to the specified value. + # 4. Update any remaining rows. + # + # These steps ensure a column can be added to a large and commonly used + # table without locking the entire table for the duration of the table + # modification. + # + # table - The name of the table to update. + # column - The name of the column to add. + # type - The column type (e.g. `:integer`). + # default - The default value for the column. + # allow_null - When set to `true` the column will allow NULL values, the + # default is to not allow NULL values. + def add_column_with_default(table, column, type, default:, allow_null: false) + if transaction_open? + raise 'add_column_with_default can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + transaction do + add_column(table, column, type, default: nil) + + # Changing the default before the update ensures any newly inserted + # rows already use the proper default value. + change_column_default(table, column, default) + end + + begin + transaction do + update_column_in_batches(table, column, default) + end + # We want to rescue _all_ exceptions here, even those that don't inherit + # from StandardError. + rescue Exception => error # rubocop: disable all + remove_column(table, column) + + raise error + end + + change_column_null(table, column, false) unless allow_null + end + end + end +end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index dccb717e95d..87a9b1e23ac 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -1,6 +1,11 @@ module Gitlab module Diff class InlineDiffMarker + MARKDOWN_SYMBOLS = { + addition: "+", + deletion: "-" + } + attr_accessor :raw_line, :rich_line def initialize(raw_line, rich_line = raw_line) @@ -8,7 +13,7 @@ module Gitlab @rich_line = ERB::Util.html_escape(rich_line) end - def mark(line_inline_diffs) + def mark(line_inline_diffs, mode: nil, markdown: false) return rich_line unless line_inline_diffs marker_ranges = [] @@ -20,13 +25,22 @@ module Gitlab end offset = 0 - # Mark each range - marker_ranges.each_with_index do |range, i| - class_names = ["idiff"] - class_names << "left" if i == 0 - class_names << "right" if i == marker_ranges.length - 1 - offset = insert_around_range(rich_line, range, "", "", offset) + # Mark each range + marker_ranges.each_with_index do |range, index| + before_content = + if markdown + "{#{MARKDOWN_SYMBOLS[mode]}" + else + "" + end + after_content = + if markdown + "#{MARKDOWN_SYMBOLS[mode]}}" + else + "" + end + offset = insert_around_range(rich_line, range, before_content, after_content, offset) end rich_line.html_safe @@ -34,6 +48,14 @@ module Gitlab private + def html_class_names(marker_ranges, mode, index) + class_names = ["idiff"] + class_names << "left" if index == 0 + class_names << "right" if index == marker_ranges.length - 1 + class_names << mode if mode + class_names.join(" ") + end + # Mapping of character positions in the raw line, to the rich (highlighted) line def position_mapping @position_mapping ||= begin diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index d0815fc7eea..522dd2b9428 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -17,16 +17,16 @@ module Gitlab Enumerator.new do |yielder| @lines.each do |line| next if filename?(line) - - full_line = line.gsub(/\n/, '') - + + full_line = line.delete("\n") + if line.match(/^@@ -/) type = "match" - + line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 - - next if line_old <= 1 && line_new <= 1 #top of file + + next if line_old <= 1 && line_new <= 1 # top of file yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next @@ -39,8 +39,8 @@ module Gitlab yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 end - - + + case line[0] when "+" line_new += 1 diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 8f9be6cd9a3..e2fee6b9f3e 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -2,22 +2,21 @@ module Gitlab module Email module Message class RepositoryPush - attr_accessor :recipient attr_reader :author_id, :ref, :action include Gitlab::Routing.url_helpers + include DiffHelper delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author delegate :username, to: :author, prefix: :author - def initialize(notify, project_id, recipient, opts = {}) + def initialize(notify, project_id, opts = {}) raise ArgumentError, 'Missing options: author_id, ref, action' unless opts[:author_id] && opts[:ref] && opts[:action] @notify = notify @project_id = project_id - @recipient = recipient @opts = opts.dup @author_id = @opts.delete(:author_id) @@ -38,7 +37,7 @@ module Gitlab end def diffs - @diffs ||= (compare.diffs if compare) + @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare) end def diffs_count @@ -49,6 +48,10 @@ module Gitlab @opts[:compare] end + def diff_refs + @opts[:diff_refs] + end + def compare_timeout diffs.overflow? if diffs end diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 6ed36b51f12..3411eb1d9ce 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -65,7 +65,7 @@ module Gitlab (l =~ /On \w+ \d+,? \d+,?.*wrote:/) # Headers on subsequent lines - break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX } + break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX } # Headers on the same line break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3 diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index 3840765db87..1918d5b208d 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -12,7 +12,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.safe_name, path: repo.path, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::INTERNAL, import_type: 'fogbugz', import_source: repo.name, - import_url: Project::UNKNOWN_IMPORT_URL + import_url: Project::UNKNOWN_IMPORT_URL, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session }) - - project end end end diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb new file mode 100644 index 00000000000..a15fc84b418 --- /dev/null +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -0,0 +1,29 @@ +module Gitlab + module GithubImport + class BranchFormatter < BaseFormatter + delegate :repo, :sha, :ref, to: :raw_data + + def exists? + project.repository.branch_exists?(ref) + end + + def name + @name ||= exists? ? ref : "#{ref}-#{short_id}" + end + + def valid? + repo.present? + end + + def valid? + repo.present? + end + + private + + def short_id + sha.to_s[0..7] + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 0f9e3ee14ee..408d9b79632 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,12 +3,15 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :project, :client + attr_reader :client, :project, :repo, :repo_url def initialize(project) - @project = project - if import_data_credentials - @client = Client.new(import_data_credentials[:user]) + @project = project + @repo = project.import_source + @repo_url = project.import_url + + if credentials + @client = Client.new(credentials[:user]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -22,12 +25,12 @@ module Gitlab private - def import_data_credentials - @import_data_credentials ||= project.import_data.credentials if project.import_data + def credentials + @credentials ||= project.import_data.credentials if project.import_data end def import_labels - client.labels(project.import_source).each do |raw_data| + client.labels(repo).each do |raw_data| Label.create!(LabelFormatter.new(project, raw_data).attributes) end @@ -37,7 +40,7 @@ module Gitlab end def import_milestones - client.list_milestones(project.import_source, state: :all).each do |raw_data| + client.list_milestones(repo, state: :all).each do |raw_data| Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) end @@ -47,9 +50,7 @@ module Gitlab end def import_issues - client.list_issues(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| + client.list_issues(repo, state: :all, sort: :created, direction: :asc).each do |raw_data| gh_issue = IssueFormatter.new(project, raw_data) if gh_issue.valid? @@ -68,29 +69,50 @@ module Gitlab end def import_pull_requests - client.pull_requests(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| - pull_request = PullRequestFormatter.new(project, raw_data) - - if pull_request.valid? - merge_request = MergeRequest.new(pull_request.attributes) - - if merge_request.save - apply_labels(pull_request.number, merge_request) - import_comments(pull_request.number, merge_request) - import_comments_on_diff(pull_request.number, merge_request) - end + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) + .map { |raw| PullRequestFormatter.new(project, raw) } + .select(&:valid?) + + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } + branches_removed = source_branches_removed | target_branches_removed + + create_refs(branches_removed) + + pull_requests.each do |pull_request| + merge_request = MergeRequest.new(pull_request.attributes) + + if merge_request.save + apply_labels(pull_request.number, merge_request) + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) end end + delete_refs(branches_removed) + true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message end + def create_refs(branches) + branches.each do |name, sha| + client.create_ref(repo, "refs/heads/#{name}", sha) + end + + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') + end + + def delete_refs(branches) + branches.each do |name, _| + client.delete_ref(repo, "heads/#{name}") + project.repository.rm_branch(project.creator, name) + end + end + def apply_labels(number, issuable) - issue = client.issue(project.import_source, number) + issue = client.issue(repo, number) if issue.labels.count > 0 label_ids = issue.labels.map do |raw| @@ -102,12 +124,12 @@ module Gitlab end def import_comments(issue_number, noteable) - comments = client.issue_comments(project.import_source, issue_number) + comments = client.issue_comments(repo, issue_number) create_comments(comments, noteable) end def import_comments_on_diff(pull_request_number, merge_request) - comments = client.pull_request_comments(project.import_source, pull_request_number) + comments = client.pull_request_comments(repo, pull_request_number) create_comments(comments, merge_request) end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index d21b942ad4b..a2947b56ad9 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,15 +1,20 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter + delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + def attributes { iid: number, title: raw_data.title, description: description, - source_project: source_project, - source_branch: source_branch.name, - target_project: target_project, - target_branch: target_branch.name, + source_project: source_branch_project, + source_branch: source_branch_name, + head_source_sha: source_branch_sha, + target_project: target_branch_project, + target_branch: target_branch_name, + base_target_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, @@ -24,7 +29,15 @@ module Gitlab end def valid? - !cross_project? && source_branch.present? && target_branch.present? + source_branch.valid? && target_branch.valid? && !cross_project? + end + + def source_branch + @source_branch ||= BranchFormatter.new(project, raw_data.head) + end + + def target_branch + @target_branch ||= BranchFormatter.new(project, raw_data.base) end private @@ -52,7 +65,7 @@ module Gitlab end def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + source_branch_repo.id != target_branch_repo.id end def description @@ -65,35 +78,10 @@ module Gitlab end end - def source_project - project - end - - def source_repo - raw_data.head.repo - end - - def source_branch - source_project.repository.find_branch(raw_data.head.ref) - end - - def target_project - project - end - - def target_repo - raw_data.base.repo - end - - def target_branch - target_project.repository.find_branch(raw_data.base.ref) - end - def state - @state ||= case true - when raw_data.state == 'closed' && raw_data.merged_at.present? + @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present? 'merged' - when raw_data.state == 'closed' + elsif raw_data.state == 'closed' 'closed' else 'opened' diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb new file mode 100644 index 00000000000..f46b43b61a4 --- /dev/null +++ b/lib/gitlab/gitignore.rb @@ -0,0 +1,56 @@ +module Gitlab + class Gitignore + FILTER_REGEX = /\.gitignore\z/.freeze + + def initialize(path) + @path = path + end + + def name + File.basename(@path, '.gitignore') + end + + def content + File.read(@path) + end + + class << self + def all + languages_frameworks + global + end + + def find(key) + file_name = "#{key}.gitignore" + + directory = select_directory(file_name) + directory ? new(File.join(directory, file_name)) : nil + end + + def global + files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) } + end + + def languages_frameworks + files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) } + end + + private + + def select_directory(file_name) + [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) } + end + + def global_dir + File.join(gitignore_dir, 'Global') + end + + def gitignore_dir + Rails.root.join('vendor/gitignore') + end + + def files_for_folder(dir) + Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') } + end + end + end +end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 96717b42bae..3f76ec97977 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -5,9 +5,9 @@ module Gitlab def initialize(project) @project = project - credentials = import_data - if credentials && credentials[:password] - @client = Client.new(credentials[:password]) + import_data = project.import_data + if import_data && import_data.credentials && import_data.credentials[:password] + @client = Client.new(import_data.credentials[:password]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -17,7 +17,7 @@ module Gitlab def execute project_identifier = CGI.escape(project.import_source) - #Issues && Comments + # Issues && Comments issues = client.issues(project_identifier) issues.each do |issue| diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 0abb7a64c17..326cfcaa8af 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::PUBLIC, import_type: "google_code", import_source: repo.name, - import_url: repo.import_url + import_url: repo.import_url, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }) - - project end end end diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/import_url.rb deleted file mode 100644 index d23b013c1f5..00000000000 --- a/lib/gitlab/import_url.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Gitlab - class ImportUrl - def initialize(url, credentials: nil) - @url = URI.parse(URI.encode(url)) - @credentials = credentials - end - - def sanitized_url - @sanitized_url ||= safe_url.to_s - end - - def credentials - @credentials ||= { user: @url.user, password: @url.password } - end - - def full_url - @full_url ||= generate_full_url.to_s - end - - private - - def generate_full_url - return @url unless valid_credentials? - @full_url = @url.dup - @full_url.user = credentials[:user] - @full_url.password = credentials[:password] - @full_url - end - - def safe_url - safe_url = @url.dup - safe_url.password = nil - safe_url.user = nil - safe_url - end - - def valid_credentials? - credentials && credentials.is_a?(Hash) && credentials.any? - end - end -end diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb new file mode 100644 index 00000000000..2a659ae4c74 --- /dev/null +++ b/lib/gitlab/lazy.rb @@ -0,0 +1,34 @@ +module Gitlab + # A class that can be wrapped around an expensive method call so it's only + # executed when actually needed. + # + # Usage: + # + # object = Gitlab::Lazy.new { some_expensive_work_here } + # + # object['foo'] + # object.bar + class Lazy < BasicObject + def initialize(&block) + @block = block + end + + def method_missing(name, *args, &block) + __evaluate__ + + @result.__send__(name, *args, &block) + end + + def respond_to_missing?(name, include_private = false) + __evaluate__ + + @result.respond_to?(name, include_private) || super + end + + private + + def __evaluate__ + @result = @block.call unless defined?(@result) + end + end +end diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index a5f767b134d..dda371e6554 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -40,7 +40,7 @@ module Gitlab # Returns boolean def plain?(filename) filename.downcase.end_with?('.txt') || - filename.downcase == 'readme' + filename.casecmp('readme').zero? end def previewable?(filename) diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 708ef79f304..0f115893a15 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -154,8 +154,6 @@ module Gitlab duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold - trans.increment(:method_duration, duration) - trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, { duration: duration }, method: #{label.inspect}) diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb index 49e5f86e6e6..8e345e8ae4a 100644 --- a/lib/gitlab/metrics/subscribers/rails_cache.rb +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -6,26 +6,28 @@ module Gitlab attach_to :active_support def cache_read(event) - increment(:cache_read_duration, event.duration) + increment(:cache_read, event.duration) end def cache_write(event) - increment(:cache_write_duration, event.duration) + increment(:cache_write, event.duration) end def cache_delete(event) - increment(:cache_delete_duration, event.duration) + increment(:cache_delete, event.duration) end def cache_exist?(event) - increment(:cache_exists_duration, event.duration) + increment(:cache_exists, event.duration) end def increment(key, duration) return unless current_transaction current_transaction.increment(:cache_duration, duration) - current_transaction.increment(key, duration) + current_transaction.increment(:cache_count, 1) + current_transaction.increment("#{key}_duration".to_sym, duration) + current_transaction.increment("#{key}_count".to_sym, 1) end private diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 50b0dd32380..5764ab15652 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -39,7 +39,7 @@ module Gitlab request_url = URI.join(base_url, project_path) domain_path = strip_url(request_url.to_s) - "\n"; + "\n" end def strip_url(url) diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb new file mode 100644 index 00000000000..56608b1b276 --- /dev/null +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -0,0 +1,24 @@ +# This Rack middleware is intended to measure the latency between +# gitlab-workhorse forwarding a request to the Rails application and the +# time this middleware is reached. + +module Gitlab + module Middleware + class RailsQueueDuration + def initialize(app) + @app = app + end + + def call(env) + trans = Gitlab::Metrics.current_transaction + proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence + if trans && proxy_start + # Time in milliseconds since gitlab-workhorse started the request + trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000) + end + + @app.call(env) + end + end + end +end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 71c5b6801fb..183bd10d6a3 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -74,7 +74,7 @@ module Gitlab end def notes - project.notes.user.search(query).order('updated_at DESC') + project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') end def commits diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 5c352c96de5..40766f35f77 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -25,7 +25,7 @@ module Gitlab end @pool.with { |redis| yield redis } end - + def self.redis_store_options url = new.url redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) @@ -40,10 +40,10 @@ module Gitlab def initialize(rails_env=nil) rails_env ||= Rails.env config_file = File.expand_path('../../../config/resque.yml', __FILE__) - + @url = "redis://localhost:6379" - if File.exists?(config_file) - @url =YAML.load_file(config_file)[rails_env] + if File.exist?(config_file) + @url = YAML.load_file(config_file)[rails_env] end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 13c4d64c99b..11c0b01f0dc 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -4,10 +4,9 @@ module Gitlab REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range) attr_accessor :project, :current_user, :author - def initialize(project, current_user = nil, author = nil) + def initialize(project, current_user = nil) @project = project @current_user = current_user - @author = author @references = {} @@ -18,17 +17,21 @@ module Gitlab super(text, context.merge(project: project)) end + def references(type) + super(type, project, current_user) + end + REFERABLES.each do |type| define_method("#{type}s") do - @references[type] ||= references(type, reference_context) + @references[type] ||= references(type) end end def issues if project && project.jira_tracker? - @references[:external_issue] ||= references(:external_issue, reference_context) + @references[:external_issue] ||= references(:external_issue) else - @references[:issue] ||= references(:issue, reference_context) + @references[:issue] ||= references(:issue) end end @@ -46,11 +49,5 @@ module Gitlab @pattern = Regexp.union(patterns.compact) end - - private - - def reference_context - { project: project, current_user: current_user, author: author } - end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index ace906a6f59..1cbd6d945a0 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -96,5 +96,9 @@ module Gitlab (? %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'altGlyphDef' => %w[id xml:base xml:lang xml:space], - 'altGlyphItem' => %w[id xml:base xml:lang xml:space], - 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'desc' => %w[class id style xml:base xml:lang xml:space], - 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], - 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], - 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feMergeNode' => %w[id xml:base xml:lang xml:space], - 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], - 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], - 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], - 'font-face-format' => %w[id string xml:base xml:lang xml:space], - 'font-face-name' => %w[id name xml:base xml:lang xml:space], - 'font-face-src' => %w[id xml:base xml:lang xml:space], - 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], - 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], - 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], - 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'metadata' => %w[id xml:base xml:lang xml:space], - 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], - 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'style' => %w[id media title type xml:base xml:lang xml:space], - 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], - 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], - 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], - 'title' => %w[class id style xml:base xml:lang xml:space], - 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], - 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], - 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], - 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] - }.freeze + ALLOWED_ATTRIBUTES = { + 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'altGlyphDef' => %w[id xml:base xml:lang xml:space], + 'altGlyphItem' => %w[id xml:base xml:lang xml:space], + 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'desc' => %w[class id style xml:base xml:lang xml:space], + 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], + 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], + 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feMergeNode' => %w[id xml:base xml:lang xml:space], + 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], + 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], + 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], + 'font-face-format' => %w[id string xml:base xml:lang xml:space], + 'font-face-name' => %w[id name xml:base xml:lang xml:space], + 'font-face-src' => %w[id xml:base xml:lang xml:space], + 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], + 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], + 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], + 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'metadata' => %w[id xml:base xml:lang xml:space], + 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'style' => %w[id media title type xml:base xml:lang xml:space], + 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], + 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'title' => %w[class id style xml:base xml:lang xml:space], + 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], + 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], + 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] + }.freeze + end end end end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 2bbbd3074e8..fe65c246101 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -62,7 +62,7 @@ module Gitlab end def wiki_page_url - "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}" + namespace_project_wiki_url(object.wiki.project.namespace, object.wiki.project, object.slug) end end end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb new file mode 100644 index 00000000000..7d02fe3c971 --- /dev/null +++ b/lib/gitlab/url_sanitizer.rb @@ -0,0 +1,54 @@ +module Gitlab + class UrlSanitizer + def self.sanitize(content) + regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git']) + + content.gsub(regexp) { |url| new(url).masked_url } + end + + def initialize(url, credentials: nil) + @url = Addressable::URI.parse(url) + @credentials = credentials + end + + def sanitized_url + @sanitized_url ||= safe_url.to_s + end + + def masked_url + url = @url.dup + url.password = "*****" unless url.password.nil? + url.user = "*****" unless url.user.nil? + url.to_s + end + + def credentials + @credentials ||= { user: @url.user, password: @url.password } + end + + def full_url + @full_url ||= generate_full_url.to_s + end + + private + + def generate_full_url + return @url unless valid_credentials? + @full_url = @url.dup + @full_url.user = credentials[:user] + @full_url.password = credentials[:password] + @full_url + end + + def safe_url + safe_url = @url.dup + safe_url.password = nil + safe_url.user = nil + safe_url + end + + def valid_credentials? + credentials && credentials.is_a?(Hash) && credentials.any? + end + end +end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index a1ee1cba216..9462f3368e6 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -32,6 +32,13 @@ module Gitlab } end + def highest_allowed_level + restricted_levels = current_application_settings.restricted_visibility_levels + + allowed_levels = self.values - restricted_levels + allowed_levels.max || PRIVATE + end + def allowed_for?(user, level) user.is_admin? || allowed_level?(level.to_i) end diff --git a/lib/json_web_token/rsa_token.rb b/lib/json_web_token/rsa_token.rb new file mode 100644 index 00000000000..d6d6af7089c --- /dev/null +++ b/lib/json_web_token/rsa_token.rb @@ -0,0 +1,42 @@ +module JSONWebToken + class RSAToken < Token + attr_reader :key_file + + def initialize(key_file) + super() + @key_file = key_file + end + + def encoded + headers = { + kid: kid + } + JWT.encode(payload, key, 'RS256', headers) + end + + private + + def key_data + @key_data ||= File.read(key_file) + end + + def key + @key ||= OpenSSL::PKey::RSA.new(key_data) + end + + def public_key + key.public_key + end + + def kid + # calculate sha256 from DER encoded ASN1 + kid = Digest::SHA256.digest(public_key.to_der) + + # we encode only 30 bytes with base32 + kid = Base32.encode(kid[0..29]) + + # insert colon every 4 characters + kid.scan(/.{4}/).join(':') + end + end +end diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb new file mode 100644 index 00000000000..5b67715b0b2 --- /dev/null +++ b/lib/json_web_token/token.rb @@ -0,0 +1,46 @@ +module JSONWebToken + class Token + attr_accessor :issuer, :subject, :audience, :id + attr_accessor :issued_at, :not_before, :expire_time + + def initialize + @id = SecureRandom.uuid + @issued_at = Time.now + # we give a few seconds for time shift + @not_before = issued_at - 5.seconds + # default 60 seconds should be more than enough for this authentication token + @expire_time = issued_at + 1.minute + @custom_payload = {} + end + + def [](key) + @custom_payload[key] + end + + def []=(key, value) + @custom_payload[key] = value + end + + def encoded + raise NotImplementedError + end + + def payload + @custom_payload.merge(default_payload) + end + + private + + def default_payload + { + jti: id, + aud: audience, + sub: subject, + iss: issuer, + iat: issued_at.to_i, + nbf: not_before.to_i, + exp: expire_time.to_i + }.compact + end + end +end diff --git a/lib/support/nginx/registry-ssl b/lib/support/nginx/registry-ssl new file mode 100644 index 00000000000..92511e26861 --- /dev/null +++ b/lib/support/nginx/registry-ssl @@ -0,0 +1,53 @@ +## Lines starting with two hashes (##) are comments with information. +## Lines starting with one hash (#) are configuration parameters that can be uncommented. +## +################################### +## configuration ## +################################### + +## Redirects all HTTP traffic to the HTTPS host +server { + listen *:80; + server_name registry.gitlab.example.com; + server_tokens off; ## Don't show the nginx version number, a security best practice + return 301 https://$http_host:$request_uri; + access_log /var/log/nginx/gitlab_registry_access.log gitlab_access; + error_log /var/log/nginx/gitlab_registry_error.log; +} + +server { + # If a different port is specified in https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/config/gitlab.yml.example#L182, + # it should be declared here as well + listen *:443 ssl http2; + server_name registry.gitlab.example.com; + server_tokens off; ## Don't show the nginx version number, a security best practice + + client_max_body_size 0; + chunked_transfer_encoding on; + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ + ssl on; + ssl_certificate /etc/gitlab/ssl/registry.gitlab.example.com.crt + ssl_certificate_key /etc/gitlab/ssl/registry.gitlab.example.com.key + + ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4'; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_session_timeout 5m; + + access_log /var/log/gitlab/nginx/gitlab_registry_access.log gitlab_access; + error_log /var/log/gitlab/nginx/gitlab_registry_error.log; + + location / { + proxy_set_header Host $http_host; # required for docker client's sake + proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 900; + + proxy_pass http://localhost:5000; + } + +} diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake deleted file mode 100644 index 16bad4bd2bd..00000000000 --- a/lib/tasks/auto_annotate_models.rake +++ /dev/null @@ -1,44 +0,0 @@ -if Rails.env.development? - task :set_annotation_options do - # You can override any of these by setting an environment variable of the - # same name. - Annotate.set_defaults( - 'routes' => 'false', - 'position_in_routes' => 'before', - 'position_in_class' => 'before', - 'position_in_test' => 'before', - 'position_in_fixture' => 'before', - 'position_in_factory' => 'before', - 'position_in_serializer' => 'before', - 'show_foreign_keys' => 'true', - 'show_indexes' => 'false', - 'simple_indexes' => 'false', - 'model_dir' => 'app/models', - 'root_dir' => '', - 'include_version' => 'false', - 'require' => '', - 'exclude_tests' => 'true', - 'exclude_fixtures' => 'true', - 'exclude_factories' => 'true', - 'exclude_serializers' => 'true', - 'exclude_scaffolds' => 'true', - 'exclude_controllers' => 'true', - 'exclude_helpers' => 'true', - 'ignore_model_sub_dir' => 'false', - 'ignore_columns' => nil, - 'ignore_unknown_models' => 'false', - 'hide_limit_column_types' => 'integer,boolean', - 'skip_on_db_migrate' => 'false', - 'format_bare' => 'true', - 'format_rdoc' => 'false', - 'format_markdown' => 'false', - 'sort' => 'false', - 'force' => 'false', - 'trace' => 'false', - 'wrapper_open' => nil, - 'wrapper_close' => nil, - ) - end - - Annotate.load_tasks -end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 402bb338f27..596eaca6d0d 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -14,6 +14,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke + Rake::Task["gitlab:backup:registry:create"].invoke backup = Backup::Manager.new backup.pack @@ -54,6 +55,7 @@ namespace :gitlab do Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') + Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry') Rake::Task['gitlab:shell:setup'].invoke backup.cleanup @@ -173,6 +175,33 @@ namespace :gitlab do end end + namespace :registry do + task create: :environment do + $progress.puts "Dumping container registry images ... ".blue + + if Gitlab.config.registry.enabled + if ENV["SKIP"] && ENV["SKIP"].include?("registry") + $progress.puts "[SKIPPED]".cyan + else + Backup::Registry.new.dump + $progress.puts "done".green + end + else + $progress.puts "[DISABLED]".cyan + end + end + + task restore: :environment do + $progress.puts "Restoring container registry images ... ".blue + if Gitlab.config.registry.enabled + Backup::Registry.new.restore + $progress.puts "done".green + else + $progress.puts "[DISABLED]".cyan + end + end + end + def configure_cron_mode if ENV['CRON'] # We need an object we can say 'puts' and 'print' to; let's use a diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index effb8eb6001..fad89c73762 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -303,7 +303,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;" + "sudo chmod 700 #{upload_path}" ) for_more_information( see_installation_guide_section "GitLab" diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 1c706dc11b3..86f5d65f128 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -29,10 +29,22 @@ namespace :gitlab do tables.delete 'schema_migrations' # Truncate schema_migrations to ensure migrations re-run connection.execute('TRUNCATE schema_migrations') + # Drop tables with cascade to avoid dependent table errors # PG: http://www.postgresql.org/docs/current/static/ddl-depend.html # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html - tables.each { |t| connection.execute("DROP TABLE #{t} CASCADE") } + # Add `IF EXISTS` because cascade could have already deleted a table. + tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{t} CASCADE") } + end + + desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' + task configure: :environment do + if ActiveRecord::Base.connection.tables.any? + Rake::Task['db:migrate'].invoke + else + Rake::Task['db:schema:load'].invoke + Rake::Task['db:seed_fu'].invoke + end end end end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake new file mode 100644 index 00000000000..84aa312002b --- /dev/null +++ b/lib/tasks/gitlab/update_gitignore.rake @@ -0,0 +1,46 @@ +namespace :gitlab do + desc "GitLab | Update gitignore" + task :update_gitignore do + unless clone_gitignores + puts "Cloning the gitignores failed".red + return + end + + remove_unneeded_files(gitignore_directory) + remove_unneeded_files(global_directory) + + puts "Done".green + end + + def clone_gitignores + FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory) + FileUtils.cd vendor_directory + + system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git') + end + + # Retain only certain files: + # - The LICENSE, because we have to + # - The sub dir global + # - The gitignores themself + # - Dir.entires returns also the entries '.' and '..' + def remove_unneeded_files(path) + Dir.foreach(path) do |file| + FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ + end + end + + private + + def vendor_directory + Rails.root.join('vendor') + end + + def gitignore_directory + File.join(vendor_directory, 'gitignore') + end + + def global_directory + File.join(gitignore_directory, 'Global') + end +end diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake index ddfaf5d51f2..78ffccc9d06 100644 --- a/lib/tasks/rubocop.rake +++ b/lib/tasks/rubocop.rake @@ -1,4 +1,5 @@ unless Rails.env.production? require 'rubocop/rake_task' + RuboCop::RakeTask.new end diff --git a/public/robots.txt b/public/robots.txt index 4f616c7f4c1..334f4c03533 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -65,3 +65,4 @@ Disallow: /*/*/deploy_keys Disallow: /*/*/hooks Disallow: /*/*/services Disallow: /*/*/protected_branches +Disallow: /*/*/uploads/ diff --git a/shared/registry/.gitkeep b/shared/registry/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 462afb24f08..6fad7e2b9e7 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -43,7 +43,7 @@ describe "mail_room.yml" do redis_config_file = Rails.root.join('config', 'resque.yml') redis_url = - if File.exists?(redis_config_file) + if File.exist?(redis_config_file) YAML.load_file(redis_config_file)[Rails.env] else "redis://localhost:6379" diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 2ba0d489197..4cb8b8da150 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -17,7 +17,7 @@ describe Admin::ProjectsController do it 'does not retrieve the project' do get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] - expect(response.body).to_not match(project.name) + expect(response.body).not_to match(project.name) end end end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index ce2a62ae1fd..6caf37ddc2c 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -114,6 +114,82 @@ describe Admin::UsersController do end end + describe 'POST update' do + context 'when the password has changed' do + def update_password(user, password, password_confirmation = nil) + params = { + id: user.to_param, + user: { + password: password, + password_confirmation: password_confirmation || password + } + } + + post :update, params + end + + context 'when the new password is valid' do + it 'redirects to the user' do + update_password(user, 'AValidPassword1') + + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'updates the password' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.encrypted_password } + end + + it 'sets the new password to expire immediately' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.password_expires_at }.to(a_value <= Time.now) + end + end + + context 'when the new password is invalid' do + it 'shows the edit page again' do + update_password(user, 'invalid') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'invalid') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/too short/)) + end + + it 'does not update the password' do + update_password(user, 'invalid') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + + context 'when the new password does not match the password confirmation' do + it 'shows the edit page again' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/doesn't match/)) + end + + it 'does not update the password' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + end + end + describe "POST impersonate" do context "when the user is blocked" do before do diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb new file mode 100644 index 00000000000..0d8a68bb51a --- /dev/null +++ b/spec/controllers/health_check_controller_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe HealthCheckController do + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + let(:xml_response) { Hash.from_xml(response.body)['hash'] } + + describe 'GET #index' do + context 'when services are up but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when services are up and an access token is provided' do + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + + it 'supports successful plaintest response' do + get :index, token: token + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + + it 'supports successful json response' do + get :index, token: token, format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + + it 'supports successful xml response' do + get :index, token: token, format: :xml + expect(response).to be_success + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be true + end + + it 'supports successful responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + end + + context 'when a service is down but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when a service is down and an access token is provided' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire') + end + + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + + it 'supports failure plaintest response' do + get :index, token: token + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + + it 'supports failure json response' do + get :index, token: token, format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('The server is on fire') + end + + it 'supports failure xml response' do + get :index, token: token, format: :xml + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be false + expect(xml_response['message']).to include('The server is on fire') + end + + it 'supports failure responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('Email is on fire') + end + end + end +end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 8ad73472117..c4b4a888b4e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -122,27 +122,23 @@ describe Projects::BranchesController do let(:branch) { "feature" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with unencoded slashes" do let(:branch) { "improve/awesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with encoded slashes" do let(:branch) { "improve%2Fawesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "invalid branch name, valid ref" do let(:branch) { "no-branch" } it { expect(response.status).to eq(404) } - it { expect(subject).to render_template('destroy') } end end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 788a609ee40..4018dac95a2 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -19,7 +19,7 @@ describe Projects::CompareController do to: ref_to) expect(response).to be_success - expect(assigns(:diffs).first).to_not be_nil + expect(assigns(:diffs).first).not_to be_nil expect(assigns(:commits).length).to be >= 1 end @@ -32,7 +32,7 @@ describe Projects::CompareController do w: 1) expect(response).to be_success - expect(assigns(:diffs).first).to_not be_nil + expect(assigns(:diffs).first).not_to be_nil expect(assigns(:commits).length).to be >= 1 # without whitespace option, there are more than 2 diff_splits diff_splits = assigns(:diffs).first.diff.split("\n") diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 40bd83af861..fbe8758dda7 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -28,7 +28,7 @@ describe Projects::GroupLinksController do expect(group.shared_projects).to include project end - it 'redirects to project group links page'do + it 'redirects to project group links page' do expect(response).to redirect_to( namespace_project_group_links_path(project.namespace, project) ) @@ -43,7 +43,7 @@ describe Projects::GroupLinksController do end it 'does not share project with that group' do - expect(group.shared_projects).to_not include project + expect(group.shared_projects).not_to include project end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 2b2ad3b9412..c469480b086 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -56,7 +56,7 @@ describe Projects::IssuesController do move_issue expect(response).to have_http_status :found - expect(another_project.issues).to_not be_empty + expect(another_project.issues).not_to be_empty end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c0a1f45195f..4f621a43d7e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid, format: format) - expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s) + expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s) end it "should not escape Html" do diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb index 4908b545648..c5d17d97ec9 100644 --- a/spec/controllers/projects/notification_settings_controller_spec.rb +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -34,5 +34,19 @@ describe Projects::NotificationSettingsController do expect(response.status).to eq 200 end end + + context 'not authorized' do + let(:private_project) { create(:project, :private) } + before { sign_in(user) } + + it 'returns 404' do + put :update, + namespace_id: private_project.namespace.to_param, + project_id: private_project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq(404) + end + end end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index ed64e7cf9af..750fbecdd07 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -38,7 +38,7 @@ describe Projects::ProjectMembersController do include_context 'import applied' it 'does not import team members' do - expect(project.team_members).to_not include member + expect(project.team_members).not_to include member end it 'responds with not found' do diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 1caa476d37d..fb29274c687 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -42,7 +42,7 @@ describe Projects::RawController do before do public_project.lfs_objects << lfs_object allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.render nothing: true } + allow(controller).to receive(:send_file) { controller.head :ok } end it 'serves the file' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 069cd917e5a..fba545560c7 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -8,6 +8,40 @@ describe ProjectsController do let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } describe "GET show" do + context "user not project member" do + before { sign_in(user) } + + context "user does not have access to project" do + let(:private_project) { create(:project, :private) } + + it "does not initialize notification setting" do + get :show, namespace_id: private_project.namespace.path, id: private_project.path + expect(assigns(:notification_setting)).to be_nil + end + end + + context "user has access to project" do + context "and does not have notification setting" do + it "initializes notification as disabled" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(assigns(:notification_setting).level).to eq("global") + end + end + + context "and has notification setting" do + before do + setting = user.notification_settings_for(public_project) + setting.level = :watch + setting.save + end + + it "shows current notification setting" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(assigns(:notification_setting).level).to eq("watch") + end + end + end + end context "rendering default project view" do render_views @@ -81,6 +115,17 @@ describe ProjectsController do expect(public_project_with_dot_atom).not_to be_valid end end + + context 'when the project is pending deletions' do + it 'renders a 404 error' do + project = create(:project, pending_delete: true) + sign_in(user) + + get :show, namespace_id: project.namespace.path, id: project.path + + expect(response.status).to eq 404 + end + end end describe "#update" do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb new file mode 100644 index 00000000000..209fa37d97d --- /dev/null +++ b/spec/controllers/registrations_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe RegistrationsController do + describe '#create' do + around(:each) do |example| + perform_enqueued_jobs do + example.run + end + end + + let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } } + + context 'when sending email confirmation' do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + + it 'logs user in directly' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last).to be_nil + expect(subject.current_user).not_to be_nil + end + end + + context 'when not sending email confirmation' do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } + + it 'does not authenticate user and sends confirmation email' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(subject.current_user).to be_nil + end + end + end +end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 83cc8ec6d26..5dc8724fb50 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -12,7 +12,7 @@ describe SessionsController do post(:create, user: { login: 'invalid', password: 'invalid' }) expect(response) - .to set_flash.now[:alert].to /Invalid login or password/ + .to set_flash.now[:alert].to /Invalid Login or password/ end end @@ -35,6 +35,27 @@ describe SessionsController do post(:create, { user: user_params }, { otp_user_id: user.id }) end + context 'remember_me field' do + it 'sets a remember_user_token cookie when enabled' do + allow(controller).to receive(:find_user).and_return(user) + expect(controller). + to receive(:remember_me).with(user).and_call_original + + authenticate_2fa(remember_me: '1', otp_attempt: user.current_otp) + + expect(response.cookies['remember_user_token']).to be_present + end + + it 'does nothing when disabled' do + allow(controller).to receive(:find_user).and_return(user) + expect(controller).not_to receive(:remember_me) + + authenticate_2fa(remember_me: '0', otp_attempt: user.current_otp) + + expect(response.cookies['remember_user_token']).to be_nil + end + end + ## # See #14900 issue # @@ -47,7 +68,7 @@ describe SessionsController do authenticate_2fa(login: another_user.username, otp_attempt: another_user.current_otp) - expect(subject.current_user).to_not eq another_user + expect(subject.current_user).not_to eq another_user end end @@ -56,7 +77,7 @@ describe SessionsController do authenticate_2fa(login: another_user.username, otp_attempt: 'invalid') - expect(subject.current_user).to_not eq another_user + expect(subject.current_user).not_to eq another_user end end @@ -73,7 +94,7 @@ describe SessionsController do before { authenticate_2fa(otp_attempt: 'invalid') } it 'does not authenticate' do - expect(subject.current_user).to_not eq user + expect(subject.current_user).not_to eq user end it 'warns about invalid OTP code' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 8045c8b940d..c61ec174665 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -112,4 +112,26 @@ describe UsersController do expect(response).to render_template('calendar_activities') end end + + describe 'GET #snippets' do + before do + sign_in(user) + end + + context 'format html' do + it 'renders snippets page' do + get :snippets, username: user.username + expect(response.status).to eq(200) + expect(response).to render_template('show') + end + end + + context 'format json' do + it 'response with snippets json data' do + get :snippets, username: user.username, format: :json + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to have_key('html') + end + end + end end diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb index d0e8c778518..8f6422a7825 100644 --- a/spec/factories/abuse_reports.rb +++ b/spec/factories/abuse_reports.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: abuse_reports -# -# id :integer not null, primary key -# reporter_id :integer -# user_id :integer -# message :text -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :abuse_report do reporter factory: :user diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index c80e7366551..efe9803b1a7 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: broadcast_messages -# -# id :integer not null, primary key -# message :text not null -# starts_at :datetime -# ends_at :datetime -# created_at :datetime -# updated_at :datetime -# color :string(255) -# font :string(255) -# - FactoryGirl.define do factory :broadcast_message do message "MyText" diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 19a54946fe0..b16c1272e68 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: forked_project_links -# -# id :integer not null, primary key -# forked_to_project_id :integer not null -# forked_from_project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :forked_project_link do association :forked_to_project, factory: :project diff --git a/spec/factories/label_links.rb b/spec/factories/label_links.rb index 2939d4307c5..3580174e873 100644 --- a/spec/factories/label_links.rb +++ b/spec/factories/label_links.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: label_links -# -# id :integer not null, primary key -# label_id :integer -# target_id :integer -# target_type :string(255) -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :label_link do label diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index ea2be8928d5..eb489099854 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: labels -# -# id :integer not null, primary key -# title :string(255) -# color :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) -# - FactoryGirl.define do factory :label do sequence(:title) { |n| "label#{n}" } diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 327858ce435..a81645acd2b 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects -# -# id :integer not null, primary key -# oid :string(255) not null -# size :integer not null -# created_at :datetime -# updated_at :datetime -# file :string(255) -# - include ActionDispatch::TestProcess FactoryGirl.define do diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb index 50b45843c99..1ed0355c8e4 100644 --- a/spec/factories/lfs_objects_projects.rb +++ b/spec/factories/lfs_objects_projects.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects_projects -# -# id :integer not null, primary key -# lfs_object_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :lfs_objects_project do lfs_object diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index e281e2f227b..c6a08d78b78 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -1,32 +1,3 @@ -# == Schema Information -# -# Table name: merge_requests -# -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) -# merge_params :text -# merge_when_build_succeeds :boolean default(FALSE), not null -# merge_user_id :integer -# merge_commit_sha :string -# - FactoryGirl.define do factory :merge_request do title diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index e5dcb159014..c32e205ee69 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -1,24 +1,3 @@ -# == Schema Information -# -# Table name: notes -# -# id :integer not null, primary key -# note :text -# noteable_type :string(255) -# author_id :integer -# created_at :datetime -# updated_at :datetime -# project_id :integer -# attachment :string(255) -# line_code :string(255) -# commit_id :string(255) -# noteable_id :integer -# system :boolean default(FALSE), not null -# st_diff :text -# updated_by_id :integer -# is_award :boolean default(FALSE), not null -# - require_relative '../support/repo_helpers' include ActionDispatch::TestProcess @@ -28,41 +7,39 @@ FactoryGirl.define do project note "Note" author + on_issue factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff] + factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] - factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] factory :downvote_note, traits: [:award, :downvote] factory :upvote_note, traits: [:award, :upvote] trait :on_commit do - project + noteable nil + noteable_id nil + noteable_type 'Commit' commit_id RepoHelpers.sample_commit.id - noteable_type "Commit" end trait :on_diff do line_code "0_184_184" end - trait :on_merge_request do - project - noteable_id 1 - noteable_type "MergeRequest" + trait :on_issue do + noteable { create(:issue, project: project) } end - trait :on_issue do - noteable_id 1 - noteable_type "Issue" + trait :on_merge_request do + noteable { create(:merge_request, source_project: project) } end trait :on_project_snippet do - noteable_id 1 - noteable_type "Snippet" + noteable { create(:snippet, project: project) } end trait :system do diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index 7700b15d538..ccf02d0719b 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: oauth_access_tokens -# -# id :integer not null, primary key -# resource_owner_id :integer -# application_id :integer -# token :string not null -# refresh_token :string -# expires_in :integer -# revoked_at :datetime -# created_at :datetime not null -# scopes :string -# - FactoryGirl.define do factory :oauth_access_token do resource_owner diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index ecd0e44dd62..da8d97c9f82 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -1,42 +1,3 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# description :text -# created_at :datetime -# updated_at :datetime -# creator_id :integer -# issues_enabled :boolean default(TRUE), not null -# merge_requests_enabled :boolean default(TRUE), not null -# wiki_enabled :boolean default(TRUE), not null -# namespace_id :integer -# issues_tracker :string(255) default("gitlab"), not null -# issues_tracker_id :string(255) -# snippets_enabled :boolean default(TRUE), not null -# last_activity_at :datetime -# import_url :string(255) -# visibility_level :integer default(0), not null -# archived :boolean default(FALSE), not null -# avatar :string(255) -# import_status :string(255) -# repository_size :float default(0.0) -# star_count :integer default(0), not null -# import_type :string(255) -# import_source :string(255) -# commit_count :integer default(0) -# import_error :text -# ci_id :integer -# builds_enabled :boolean default(TRUE), not null -# shared_runners_enabled :boolean default(TRUE), not null -# runners_token :string -# build_coverage_regex :string -# build_allow_git_fetch :boolean default(TRUE), not null -# build_timeout :integer default(3600), not null -# - FactoryGirl.define do # Project without repository # @@ -60,6 +21,12 @@ FactoryGirl.define do trait :private do visibility_level Gitlab::VisibilityLevel::PRIVATE end + + trait :empty_repo do + after(:create) do |project| + project.create_repository + end + end end # Project with empty repository @@ -67,9 +34,7 @@ FactoryGirl.define do # This is a case when you just created a project # but not pushed any code there yet factory :project_empty_repo, parent: :empty_project do - after :create do |project| - project.create_repository - end + empty_repo end # Project with test repository diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 7f331c37256..74497dc82c0 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: releases -# -# id :integer not null, primary key -# tag :string(255) -# description :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :release do tag "v1.1.0" diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 7ae06c27840..f426e27afed 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -1,21 +1,3 @@ -# == Schema Information -# -# Table name: todos -# -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# target_id :integer -# target_type :string not null -# author_id :integer -# action :integer not null -# state :string not null -# created_at :datetime -# updated_at :datetime -# note_id :integer -# commit_id :string -# - FactoryGirl.define do factory :todo do project @@ -36,5 +18,9 @@ FactoryGirl.define do commit_id RepoHelpers.sample_commit.id target_type "Commit" end + + trait :build_failed do + action { Todo::BUILD_FAILED } + end end end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 62de081661d..675d9bd18b7 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -5,8 +5,8 @@ describe 'factories' do describe "#{factory.name} factory" do let(:entity) { build(factory.name) } - it 'does not raise error when created 'do - expect { entity }.to_not raise_error + it 'does not raise error when created' do + expect { entity }.not_to raise_error end it 'should be valid', if: factory.build_class < ActiveRecord::Base do diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 2e9851fb442..7bbe20fec43 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -19,6 +19,7 @@ describe 'Admin Builds' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page).to have_selector('.row-content-block', text: 'All builds') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb new file mode 100644 index 00000000000..dec2dedf2b5 --- /dev/null +++ b/spec/features/admin/admin_health_check_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature "Admin Health Check", feature: true do + include WaitForAjax + + before do + login_as :admin + end + + describe '#show' do + before do + visit admin_health_check_path + end + + it { page.has_text? 'Health Check' } + it { page.has_text? 'Health information can be retrieved' } + + it 'has a health check access token' do + token = current_application_settings.health_check_access_token + expect(page).to have_content("Access token is #{token}") + expect(page).to have_selector('#health-check-token', text: token) + end + + describe 'reload access token', js: true do + it 'changes the access token' do + orig_token = current_application_settings.health_check_access_token + click_button 'Reset health check access token' + wait_for_ajax + expect(find('#health-check-token').text).not_to eq orig_token + end + end + end + + context 'when services are up' do + before do + visit admin_health_check_path + end + + it 'shows healthy status' do + expect(page).to have_content('Current Status: Healthy') + end + end + + context 'when a service is down' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).and_return('The server is on fire') + visit admin_health_check_path + end + + it 'shows unhealthy status' do + expect(page).to have_content('Current Status: Unhealthy') + expect(page).to have_content('The server is on fire') + end + end +end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 26d03944b8a..8ebd4a6808e 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -79,7 +79,7 @@ describe "Admin Runners" do end it 'changes registration token' do - expect(page_token).to_not eq token + expect(page_token).not_to eq token end end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 4570e409128..96621843b30 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -152,7 +152,7 @@ describe "Admin::Users", feature: true do it 'sees impersonation log out icon' do icon = first('.fa.fa-user-secret') - expect(icon).to_not eql nil + expect(icon).not_to eql nil end it 'can log out of impersonated user back to original user' do @@ -210,6 +210,8 @@ describe "Admin::Users", feature: true do before do fill_in "user_name", with: "Big Bang" fill_in "user_email", with: "bigbang@mail.com" + fill_in "user_password", with: "AValidPassword1" + fill_in "user_password_confirmation", with: "AValidPassword1" check "user_admin" click_button "Save changes" end @@ -223,6 +225,7 @@ describe "Admin::Users", feature: true do @simple_user.reload expect(@simple_user.name).to eq('Big Bang') expect(@simple_user.is_admin?).to be_truthy + expect(@simple_user.password_expires_at).to be <= Time.now end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 090a941958f..7a05d30e8b5 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -46,7 +46,7 @@ describe "Builds" do it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } - it { expect(page).to_not have_link 'Cancel running' } + it { expect(page).not_to have_link 'Cancel running' } end end @@ -62,7 +62,7 @@ describe "Builds" do it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } - it { expect(page).to_not have_link 'Cancel running' } + it { expect(page).not_to have_link 'Cancel running' } end describe "GET /:project/builds/:id" do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index dacaa96d760..20f0b27bcc1 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -137,8 +137,8 @@ describe 'Commits' do expect(page).to have_content commit.git_commit_message expect(page).to have_content commit.git_author_name expect(page).to have_link('Download artifacts') - expect(page).to_not have_link('Cancel running') - expect(page).to_not have_link('Retry failed') + expect(page).not_to have_link('Cancel running') + expect(page).not_to have_link('Retry failed') end end @@ -155,9 +155,9 @@ describe 'Commits' do expect(page).to have_content commit.sha[0..7] expect(page).to have_content commit.git_commit_message expect(page).to have_content commit.git_author_name - expect(page).to_not have_link('Download artifacts') - expect(page).to_not have_link('Cancel running') - expect(page).to_not have_link('Retry failed') + expect(page).not_to have_link('Download artifacts') + expect(page).not_to have_link('Cancel running') + expect(page).not_to have_link('Retry failed') end end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb new file mode 100644 index 00000000000..53b4f027117 --- /dev/null +++ b/spec/features/container_registry_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe "Container Registry" do + let(:project) { create(:empty_project) } + let(:repository) { project.container_registry_repository } + let(:tag_name) { 'latest' } + let(:tags) { [tag_name] } + + before do + login_as(:user) + project.team << [@user, :developer] + stub_container_registry_tags(*tags) + stub_container_registry_config(enabled: true) + allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') + end + + describe 'GET /:project/container_registry' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + context 'when no tags' do + let(:tags) { [] } + + it { expect(page).to have_content('No images in Container Registry for this project') } + end + + context 'when there are tags' do + it { expect(page).to have_content(tag_name)} + end + end + + describe 'DELETE /:project/container_registry/tag' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + it do + expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) + + click_on 'Remove' + end + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 192e3619375..7efbaaa048c 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -154,4 +154,180 @@ describe 'Filter issues', feature: true do end end end + + describe 'filter issues by text' do + before do + create(:issue, title: "Bug", project: project) + + bug_label = create(:label, project: project, title: 'bug') + milestone = create(:milestone, title: "8", project: project) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'only text', js: true do + it 'should filter issues by searched text' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + end + + it 'should not show any issues' do + fill_in 'issue_search', with: 'testing' + + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + end + + context 'text and dropdown options', js: true do + it 'should filter by text and label' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Label' + page.within '.labels-filter' do + click_link 'bug' + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + + it 'should filter by text and milestone' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Milestone' + page.within '.milestone-filter' do + click_link '8' + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + + it 'should filter by text and assignee' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Assignee' + page.within '.dropdown-menu-assignee' do + click_link user.name + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + + it 'should filter by text and author' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Author' + page.within '.dropdown-menu-author' do + click_link user.name + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + end + end + + describe 'filter issues and sort', js: true do + before do + bug_label = create(:label, project: project, title: 'bug') + bug_one = create(:issue, title: "Frontend", project: project) + bug_two = create(:issue, title: "Bug 2", project: project) + + bug_one.labels << bug_label + bug_two.labels << bug_label + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'should be able to filter and sort issues' do + click_button 'Label' + page.within '.labels-filter' do + click_link 'bug' + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Last created' + page.within '.dropdown-menu-sort' do + click_link 'Oldest created' + end + + page.within '.issues-list' do + expect(first('.issue')).to have_content('Frontend') + end + end + end + + describe 'filter by any author', js: true do + before do + user2 = create(:user, name: "tester") + create(:issue, project: project, author: user) + create(:issue, project: project, author: user2) + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'should show filter by any author link' do + click_button "Author" + fill_in "Search authors", with: "tester" + + page.within ".dropdown-menu-author" do + expect(page).to have_content "tester" + end + end + + it 'should show filter issues by any author' do + page.within '.issues-list' do + expect(page).to have_selector ".issue", count: 2 + end + + click_button "Author" + fill_in "Search authors", with: "tester" + + page.within ".dropdown-menu-author" do + click_link "tester" + end + + page.within '.issues-list' do + expect(page).to have_selector ".issue", count: 1 + end + end + end end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 84c8e20ebaa..c7019c5aea1 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -19,7 +19,7 @@ feature 'issue move to another project' do end scenario 'moving issue to another project not allowed' do - expect(page).to have_no_select('move_to_project_id') + expect(page).to have_no_selector('#move_to_project_id') end end @@ -37,7 +37,7 @@ feature 'issue move to another project' do end scenario 'moving issue to another project' do - select(new_project.name_with_namespace, from: 'move_to_project_id') + first('#move_to_project_id', visible: false).set(new_project.id) click_button('Save changes') expect(current_url).to include project_path(new_project) @@ -47,14 +47,18 @@ feature 'issue move to another project' do expect(page).to have_content(issue.title) end - context 'projects user does not have permission to move issue to exist' do + context 'user does not have permission to move the issue to a project', js: true do let!(:private_project) { create(:project, :private) } let(:another_project) { create(:project) } background { another_project.team << [user, :guest] } scenario 'browsing projects in projects select' do - options = [ '', 'No project', new_project.name_with_namespace ] - expect(page).to have_select('move_to_project_id', options: options) + click_link 'Select project' + + page.within '.select2-results' do + expect(page).to have_content 'No project' + expect(page).to have_content new_project.name_with_namespace + end end end @@ -65,7 +69,7 @@ feature 'issue move to another project' do end scenario 'user wants to move issue that has already been moved' do - expect(page).to have_no_select('move_to_project_id') + expect(page).to have_no_selector('#move_to_project_id') end end end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index e4efdbe2421..f5cfe2d666e 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -9,8 +9,11 @@ feature 'Issue notes polling' do end scenario 'Another user adds a comment to an issue', js: true do - note = create(:note_on_issue, noteable: issue, note: 'Looks good!') + note = create(:note, noteable: issue, project: project, + note: 'Looks good!') + page.execute_script('notes.refresh();') + expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index b03dd0f666d..466a6f7dfa7 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -95,7 +95,7 @@ feature 'Multiple issue updating from issues#index', feature: true do find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button - expect(first('.issue')).to_not have_content milestone.title + expect(first('.issue')).not_to have_content milestone.title end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d5755c293c5..9271964166a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -64,10 +64,68 @@ describe 'Issues', feature: true do end end + describe 'due date', js: true do + context 'on new form' do + before do + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'should save with due date' do + date = Date.today.at_beginning_of_month + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + page.within '.datepicker' do + click_link date.day + end + + expect(find('#issuable-due-date', visible: false).value).to eq date.to_s + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + + context 'on edit form' do + let(:issue) { create(:issue, author: @user,project: project, due_date: Date.today.at_beginning_of_month.to_s) } + + before do + visit edit_namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should save with due date' do + date = Date.today.at_beginning_of_month + + expect(find('#issuable-due-date', visible: false).value).to eq date.to_s + + date = date.tomorrow + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + page.within '.datepicker' do + click_link date.day + end + + expect(find('#issuable-due-date', visible: false).value).to eq date.to_s + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + end + describe 'Issue info' do it 'excludes award_emoji from comment count' do issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') - create(:upvote_note, noteable: issue) + create(:upvote_note, noteable: issue, project: project) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) @@ -331,7 +389,7 @@ describe 'Issues', feature: true do page.within '.assignee' do click_link 'Edit' end - + page.within '.dropdown-menu-user' do click_link @user.name end @@ -431,6 +489,43 @@ describe 'Issues', feature: true do end end + describe 'due date' do + context 'update due on issue#show', js: true do + let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } + + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should add due date to issue' do + page.within '.due_date' do + click_link 'Edit' + + page.within '.ui-datepicker-calendar' do + first('.ui-state-default').click + end + + expect(page).to have_no_content 'None' + end + end + + it 'should remove due date from issue' do + page.within '.due_date' do + click_link 'Edit' + + page.within '.ui-datepicker-calendar' do + first('.ui-state-default').click + end + + expect(page).to have_no_content 'None' + + click_link 'remove due date' + expect(page).to have_content 'None' + end + end + end + end + def first_issue page.all('ul.issues-list > li').first.text end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 8c38dd5b122..c1b178c3b6c 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -32,7 +32,7 @@ feature 'Login', feature: true do let(:user) { create(:user, :two_factor) } before do - login_with(user) + login_with(user, remember: true) expect(page).to have_content('Two-factor Authentication') end @@ -52,6 +52,12 @@ feature 'Login', feature: true do expect(current_path).to eq root_path end + it 'persists remember_me value via hidden field' do + field = first('input#user_remember_me', visible: false) + + expect(field.value).to eq '1' + end + it 'blocks login with invalid code' do enter_code('foo') expect(page).to have_content('Invalid two-factor code') @@ -121,7 +127,7 @@ feature 'Login', feature: true do user = create(:user, password: 'not-the-default') login_with(user) - expect(page).to have_content('Invalid login or password.') + expect(page).to have_content('Invalid Login or password.') end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 0148c87084a..1d892fe1a55 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -278,6 +278,10 @@ describe 'GitLab Markdown', feature: true do it 'includes GollumTagsFilter' do expect(doc).to parse_gollum_tags end + + it 'includes InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end end # Fake a `current_user` helper diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb new file mode 100644 index 00000000000..edc0bdec3db --- /dev/null +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +feature 'Merge request created from fork' do + given(:user) { create(:user) } + given(:project) { create(:project, :public) } + given(:fork_project) { create(:project, :public) } + + given!(:merge_request) do + create(:forked_project_link, forked_to_project: fork_project, + forked_from_project: project) + + create(:merge_request_with_diffs, source_project: fork_project, + target_project: project, + description: 'Test merge request') + end + + background do + fork_project.team << [user, :master] + login_as user + end + + scenario 'user can access merge request' do + visit_merge_request(merge_request) + + expect(page).to have_content 'Test merge request' + end + + context 'pipeline present in source project' do + include WaitForAjax + + given(:pipeline) do + create(:ci_commit_with_two_jobs, project: fork_project, + sha: merge_request.last_commit.id, + ref: merge_request.source_branch) + end + + background { pipeline.create_builds(user) } + + scenario 'user visits a pipelines page', js: true do + visit_merge_request(merge_request) + page.within('.merge-request-tabs') { click_link 'Builds' } + wait_for_ajax + + page.within('table.builds') do + expect(page).to have_content 'rspec' + expect(page).to have_content 'spinach' + end + + expect(find_link('Cancel running')[:href]) + .to include fork_project.path_with_namespace + end + end + + def visit_merge_request(mr) + visit namespace_project_merge_request_path(project.namespace, + project, mr) + end +end diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb new file mode 100644 index 00000000000..1c130057c56 --- /dev/null +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -0,0 +1,161 @@ +require 'spec_helper' + +describe 'Projects > Merge requests > User lists merge requests', feature: true do + include SortingHelper + + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + before do + @fix = create(:merge_request, + title: 'fix', + source_project: project, + source_branch: 'fix', + assignee: user, + milestone: create(:milestone, due_date: '2013-12-11'), + created_at: 1.minute.ago, + updated_at: 1.minute.ago) + create(:merge_request, + title: 'markdown', + source_project: project, + source_branch: 'markdown', + assignee: user, + milestone: create(:milestone, due_date: '2013-12-12'), + created_at: 2.minutes.ago, + updated_at: 2.minutes.ago) + create(:merge_request, + title: 'lfs', + source_project: project, + source_branch: 'lfs', + created_at: 3.minutes.ago, + updated_at: 10.seconds.ago) + end + + it 'filters on no assignee' do + visit_merge_requests(project, assignee_id: IssuableFinder::NONE) + + expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project)) + expect(page).to have_content 'lfs' + expect(page).not_to have_content 'fix' + expect(page).not_to have_content 'markdown' + expect(count_merge_requests).to eq(1) + end + + it 'filters on a specific assignee' do + visit_merge_requests(project, assignee_id: user.id) + + expect(page).not_to have_content 'lfs' + expect(page).to have_content 'fix' + expect(page).to have_content 'markdown' + expect(count_merge_requests).to eq(2) + end + + it 'sorts by newest' do + visit_merge_requests(project, sort: sort_value_recently_created) + + expect(first_merge_request).to include('lfs') + expect(last_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by oldest' do + visit_merge_requests(project, sort: sort_value_oldest_created) + + expect(first_merge_request).to include('fix') + expect(last_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by last updated' do + visit_merge_requests(project, sort: sort_value_recently_updated) + + expect(first_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by oldest updated' do + visit_merge_requests(project, sort: sort_value_oldest_updated) + + expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by milestone due soon' do + visit_merge_requests(project, sort: sort_value_milestone_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by milestone due later' do + visit_merge_requests(project, sort: sort_value_milestone_later) + + expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) + end + + it 'filters on one label and sorts by due soon' do + label = create(:label, project: project) + create(:label_link, label: label, target: @fix) + + visit_merge_requests(project, label_name: [label.name], + sort: sort_value_due_date_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) + end + + context 'while filtering on two labels' do + let(:label) { create(:label, project: project) } + let(:label2) { create(:label, project: project) } + + before do + create(:label_link, label: label, target: @fix) + create(:label_link, label: label2, target: @fix) + end + + it 'sorts by due soon' do + visit_merge_requests(project, label_name: [label.name, label2.name], + sort: sort_value_due_date_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) + end + + context 'filter on assignee and' do + it 'sorts by due soon' do + visit_merge_requests(project, label_name: [label.name, label2.name], + assignee_id: user.id, + sort: sort_value_due_date_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) + end + + it 'sorts by recently due milestone' do + visit namespace_project_merge_requests_path(project.namespace, project, + label_name: [label.name, label2.name], + assignee_id: user.id, + sort: sort_value_milestone_soon) + + expect(first_merge_request).to include('fix') + end + end + end + + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path(project.namespace, project, opts) + end + + def first_merge_request + page.all('ul.mr-list > li').first.text + end + + def last_merge_request + page.all('ul.mr-list > li').last.text + end + + def count_merge_requests + page.all('ul.mr-list > li').count + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 389812ff7e1..2835cf44494 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -19,10 +19,14 @@ describe 'Comments', feature: true do end describe 'On a merge request', js: true, feature: true do - let!(:merge_request) { create(:merge_request) } - let!(:project) { merge_request.source_project } + let!(:project) { create(:project) } + let!(:merge_request) do + create(:merge_request, source_project: project, target_project: project) + end + let!(:note) do - create(:note_on_merge_request, :with_attachment, project: project) + create(:note_on_merge_request, :with_attachment, noteable: merge_request, + project: project) end before do @@ -192,7 +196,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[id$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}-true']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index 1adab7e9c6c..c7c00a3266a 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -32,7 +32,8 @@ feature 'Member autocomplete', feature: true do context 'adding a new note on a Issue', js: true do before do issue = create(:issue, author: author, project: project) - create(:note, note: 'Ultralight Beam', noteable: issue, author: participant) + create(:note, note: 'Ultralight Beam', noteable: issue, + project: project, author: participant) visit_issue(project, issue) end @@ -47,7 +48,8 @@ feature 'Member autocomplete', feature: true do context 'adding a new note on a Merge Request ', js: true do before do merge = create(:merge_request, source_project: project, target_project: project, author: author) - create(:note, note: 'Ultralight Beam', noteable: merge, author: participant) + create(:note, note: 'Ultralight Beam', noteable: merge, + project: project, author: participant) visit_merge_request(project, merge) end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb new file mode 100644 index 00000000000..acd6fb3538c --- /dev/null +++ b/spec/features/pipelines_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe "Pipelines" do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + login_as(user) + project.team << [user, :developer] + end + + describe 'GET /:project/pipelines' do + let!(:pipeline) { create(:ci_commit, project: project, ref: 'master', status: 'running') } + + [:all, :running, :branches].each do |scope| + context "displaying #{scope}" do + let(:project) { create(:project) } + + before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } + + it { expect(page).to have_content(pipeline.short_sha) } + end + end + + context 'anonymous access' do + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_http_status(:success) } + end + + context 'cancelable pipeline' do + let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Cancel') } + it { expect(page).to have_selector('.ci-running') } + + context 'when canceling' do + before { click_link('Cancel') } + + it { expect(page).not_to have_link('Cancel') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'retryable pipelines' do + let!(:failed) { create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Retry') } + it { expect(page).to have_selector('.ci-failed') } + + context 'when retrying' do + before { click_link('Retry') } + + it { expect(page).not_to have_link('Retry') } + it { expect(page).to have_selector('.ci-pending') } + end + end + + context 'for generic statuses' do + context 'when running' do + let!(:running) { create(:generic_commit_status, status: 'running', commit: pipeline, stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it 'not be cancelable' do + expect(page).not_to have_link('Cancel') + end + + it 'pipeline is running' do + expect(page).to have_selector('.ci-running') + end + end + + context 'when failed' do + let!(:running) { create(:generic_commit_status, status: 'failed', commit: pipeline, stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it 'not be retryable' do + expect(page).not_to have_link('Retry') + end + + it 'pipeline is failed' do + expect(page).to have_selector('.ci-failed') + end + end + end + + context 'downloadable pipelines' do + context 'with artifacts' do + let!(:with_artifacts) { create(:ci_build, :artifacts, :success, commit: pipeline, name: 'rspec tests', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_selector('.build-artifacts') } + it { expect(page).to have_link(with_artifacts.name) } + end + + context 'without artifacts' do + let!(:without_artifacts) { create(:ci_build, :success, commit: pipeline, name: 'rspec', stage: 'test') } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + end + end + + describe 'GET /:project/pipelines/:id' do + let(:pipeline) { create(:ci_commit, project: project, ref: 'master') } + + before do + @success = create(:ci_build, :success, commit: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, commit: pipeline, stage: 'deploy', name: 'deploy') + @external = create(:generic_commit_status, status: 'success', commit: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + + it 'showing a list of builds' do + expect(page).to have_content('Tests') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + it { expect(page).to have_content('retried') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + end + + describe 'POST /:project/pipelines' do + let(:project) { create(:project) } + + before { visit new_namespace_project_pipeline_path(project.namespace, project) } + + context 'for valid commit' do + before { fill_in('Create for', with: 'master') } + + context 'with gitlab-ci.yml' do + before { stub_ci_commit_to_return_yaml_file } + + it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) } + end + + context 'without gitlab-ci.yml' do + before { click_on 'Create pipeline' } + + it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + end + end + + context 'for invalid commit' do + before do + fill_in('Create for', with: 'invalid reference') + click_on 'Create pipeline' + end + + it { expect(page).to have_content('Reference not found') } + end + end +end diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/project/shortcuts_spec.rb deleted file mode 100644 index 2595c4181e5..00000000000 --- a/spec/features/project/shortcuts_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -feature 'Project shortcuts', feature: true do - let(:project) { create(:project) } - let(:user) { create(:user) } - - describe 'On a project', js: true do - before do - project.team << [user, :master] - login_as user - visit namespace_project_path(project.namespace, project) - end - - describe 'pressing "i"' do - it 'redirects to new issue page' do - find('body').native.send_key('i') - expect(page).to have_content('New Issue') - end - end - end -end diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 13c9b95b316..51be81d634c 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -8,12 +8,10 @@ feature 'list of badges' do project = create(:project) project.team << [user, :master] login_as(user) - visit edit_namespace_project_path(project.namespace, project) + visit namespace_project_badges_path(project.namespace, project) end scenario 'user displays list of badges' do - click_link 'Badges' - expect(page).to have_content 'build status' expect(page).to have_content 'Markdown' expect(page).to have_content 'HTML' @@ -26,7 +24,6 @@ feature 'list of badges' do end scenario 'user changes current ref on badges list page', js: true do - click_link 'Badges' select2('improve/awesome', from: '#ref') expect(page).to have_content 'badges/improve/awesome/build.svg' diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb new file mode 100644 index 00000000000..0c51fe72ca4 --- /dev/null +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +feature 'Developer views empty project instructions', feature: true do + let(:project) { create(:empty_project, :empty_repo) } + let(:developer) { create(:user) } + + background do + project.team << [developer, :developer] + + login_as(developer) + end + + context 'without an SSH key' do + scenario 'defaults to HTTP' do + visit_project + + expect_instructions_for('http') + end + + scenario 'switches to SSH', js: true do + visit_project + + select_protocol('SSH') + + expect_instructions_for('ssh') + end + end + + context 'with an SSH key' do + background do + create(:personal_key, user: developer) + end + + scenario 'defaults to SSH' do + visit_project + + expect_instructions_for('ssh') + end + + scenario 'switches to HTTP', js: true do + visit_project + + select_protocol('HTTP') + + expect_instructions_for('http') + end + end + + def visit_project + visit namespace_project_path(project.namespace, project) + end + + def select_protocol(protocol) + find('#clone-dropdown').click + find(".#{protocol.downcase}-selector").click + end + + def expect_instructions_for(protocol) + msg = :"#{protocol.downcase}_url_to_repo" + + expect(page).to have_content("git clone #{project.send(msg)}") + end +end diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb new file mode 100644 index 00000000000..073a83b6896 --- /dev/null +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a .gitignore file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore') + end + + scenario 'user can see .gitignore dropdown' do + expect(page).to have_css('.gitignore-selector') + end + + scenario 'user can pick a .gitignore file from the dropdown', js: true do + find('.js-gitignore-selector').click + wait_for_ajax + within '.gitignore-selector' do + find('.dropdown-input-field').set('rails') + find('.dropdown-content li', text: 'Rails').click + end + wait_for_ajax + + expect(page).to have_content('/.bundle') + expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset') + end +end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 3d6ffbc4c6b..ecc818eb1e1 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true click_button 'Commit Changes' @@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end scenario 'project master creates a license file from the "Add license" link' do @@ -48,7 +48,7 @@ feature 'project owner creates a license file', feature: true, js: true do file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true click_button 'Commit Changes' @@ -56,6 +56,6 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 3268e240200..34eda29c285 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true # Remove pre-receive hook so we can push without auth @@ -34,6 +34,6 @@ feature 'project owner sees a link to create a license file in empty project', f expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end end diff --git a/spec/features/projects/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb new file mode 100644 index 00000000000..54aa9c66a08 --- /dev/null +++ b/spec/features/projects/shortcuts_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Project shortcuts', feature: true do + let(:project) { create(:project, name: 'Victorialand') } + let(:user) { create(:user) } + + describe 'On a project', js: true do + before do + project.team << [user, :master] + login_as user + visit namespace_project_path(project.namespace, project) + end + + describe 'pressing "i"' do + it 'redirects to new issue page' do + find('body').native.send_key('i') + expect(page).to have_content('Victorialand') + end + end + end +end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 8edeb8d18af..a5ed3595b0a 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -29,8 +29,8 @@ describe "Runners" do end before do - expect(page).to_not have_content(@specific_runner3.display_name) - expect(page).to_not have_content(@specific_runner3.display_name) + expect(page).not_to have_content(@specific_runner3.display_name) + expect(page).not_to have_content(@specific_runner3.display_name) end it "places runners in right places" do @@ -110,4 +110,37 @@ describe "Runners" do expect(page).to have_content(@specific_runner.platform) end end + + feature 'configuring runners ability to picking untagged jobs' do + given(:project) { create(:empty_project) } + given(:runner) { create(:ci_runner) } + + background do + project.team << [user, :master] + project.runners << runner + end + + scenario 'user checks default configuration' do + visit namespace_project_runner_path(project.namespace, project, runner) + + expect(page).to have_content 'Can run untagged jobs Yes' + end + + context 'when runner has tags' do + before { runner.update_attribute(:tag_list, ['tag']) } + + scenario 'user wants to prevent runner from running untagged job' do + visit runners_path(project) + page.within('.activated-specific-runners') do + first('small > a').click + end + + uncheck 'runner_run_untagged' + click_button 'Save changes' + + expect(page).to have_content 'Can run untagged jobs No' + expect(runner.reload.run_untagged?).to eq false + end + end + end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 58aabd913eb..4229e82b443 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,20 +2,45 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do - it 'creates the user account and sends a confirmation email' do - user = build(:user) - visit root_path + context "when sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Sign up" + it 'creates the user account and sends a confirmation email' do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") + end end + + context "when not sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + + it 'creates the user account and goes to dashboard' do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + end describe 'signup with errors' do diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb new file mode 100644 index 00000000000..08a97085a9c --- /dev/null +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +feature 'Master creates tag', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + project.team << [user, :master] + login_with(user) + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'with an invalid name displays an error' do + create_tag_in_form(tag: 'v 1.0', ref: 'master') + + expect(page).to have_content 'Tag name invalid' + end + + scenario 'with an invalid reference displays an error' do + create_tag_in_form(tag: 'v2.0', ref: 'foo') + + expect(page).to have_content 'Target foo is invalid' + end + + scenario 'that already exists displays an error' do + create_tag_in_form(tag: 'v1.1.0', ref: 'master') + + expect(page).to have_content 'Tag v1.1.0 already exists' + end + + scenario 'with multiline message displays the message in a
     block' do
    +    create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world")
    +
    +    expect(current_path).to eq(
    +      namespace_project_tag_path(project.namespace, project, 'v3.0'))
    +    expect(page).to have_content 'v3.0'
    +    page.within 'pre.body' do
    +      expect(page).to have_content "Awesome tag message\n\n- hello\n- world"
    +    end
    +  end
    +
    +  scenario 'with multiline release notes parses the release note as Markdown' do
    +    create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world")
    +
    +    expect(current_path).to eq(
    +      namespace_project_tag_path(project.namespace, project, 'v4.0'))
    +    expect(page).to have_content 'v4.0'
    +    page.within '.description' do
    +      expect(page).to have_content 'Awesome release notes'
    +      expect(page).to have_selector('ul li', count: 2)
    +    end
    +  end
    +
    +  def create_tag_in_form(tag:, ref:, message: nil, desc: nil)
    +    click_link 'New tag'
    +    fill_in 'tag_name', with: tag
    +    fill_in 'ref', with: ref
    +    fill_in 'message', with: message unless message.nil?
    +    fill_in 'release_description', with: desc unless desc.nil?
    +    click_button 'Create tag'
    +  end
    +end
    diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
    new file mode 100644
    index 00000000000..f0990118e3c
    --- /dev/null
    +++ b/spec/features/tags/master_deletes_tag_spec.rb
    @@ -0,0 +1,41 @@
    +require 'spec_helper'
    +
    +feature 'Master deletes tag', feature: true do
    +  let(:user) { create(:user) }
    +  let(:project) { create(:project, namespace: user.namespace) }
    +
    +  before do
    +    project.team << [user, :master]
    +    login_with(user)
    +    visit namespace_project_tags_path(project.namespace, project)
    +  end
    +
    +  context 'from the tags list page' do
    +    scenario 'deletes the tag' do
    +      expect(page).to have_content 'v1.1.0'
    +
    +      page.within('.content') do
    +        first('.btn-remove').click
    +      end
    +
    +      expect(current_path).to eq(
    +        namespace_project_tags_path(project.namespace, project))
    +      expect(page).not_to have_content 'v1.1.0'
    +    end
    +
    +  end
    +
    +  context 'from a specific tag page' do
    +    scenario 'deletes the tag' do
    +      click_on 'v1.0.0'
    +      expect(current_path).to eq(
    +        namespace_project_tag_path(project.namespace, project, 'v1.0.0'))
    +
    +      click_on 'Delete tag'
    +
    +      expect(current_path).to eq(
    +        namespace_project_tags_path(project.namespace, project))
    +      expect(page).not_to have_content 'v1.0.0'
    +    end
    +  end
    +end
    diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
    new file mode 100644
    index 00000000000..6b5b3122f72
    --- /dev/null
    +++ b/spec/features/tags/master_updates_tag_spec.rb
    @@ -0,0 +1,42 @@
    +require 'spec_helper'
    +
    +feature 'Master updates tag', feature: true do
    +  let(:user) { create(:user) }
    +  let(:project) { create(:project, namespace: user.namespace) }
    +
    +  before do
    +    project.team << [user, :master]
    +    login_with(user)
    +    visit namespace_project_tags_path(project.namespace, project)
    +  end
    +
    +  context 'from the tags list page' do
    +    scenario 'updates the release notes' do
    +      page.within(first('.content-list .controls')) do
    +        click_link 'Edit release notes'
    +      end
    +
    +      fill_in 'release_description', with: 'Awesome release notes'
    +      click_button 'Save changes'
    +
    +      expect(current_path).to eq(
    +        namespace_project_tag_path(project.namespace, project, 'v1.1.0'))
    +      expect(page).to have_content 'v1.1.0'
    +      expect(page).to have_content 'Awesome release notes'
    +    end
    +  end
    +
    +  context 'from a specific tag page' do
    +    scenario 'updates the release notes' do
    +      click_on 'v1.1.0'
    +      click_link 'Edit release notes'
    +      fill_in 'release_description', with: 'Awesome release notes'
    +      click_button 'Save changes'
    +
    +      expect(current_path).to eq(
    +        namespace_project_tag_path(project.namespace, project, 'v1.1.0'))
    +      expect(page).to have_content 'v1.1.0'
    +      expect(page).to have_content 'Awesome release notes'
    +    end
    +  end
    +end
    diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
    new file mode 100644
    index 00000000000..29d2c244720
    --- /dev/null
    +++ b/spec/features/tags/master_views_tags_spec.rb
    @@ -0,0 +1,73 @@
    +require 'spec_helper'
    +
    +feature 'Master views tags', feature: true do
    +  let(:user) { create(:user) }
    +
    +  before do
    +    project.team << [user, :master]
    +    login_with(user)
    +  end
    +
    +  context 'when project has no tags' do
    +    let(:project) { create(:project_empty_repo) }
    +    before do
    +      visit namespace_project_path(project.namespace, project)
    +      click_on 'README'
    +      fill_in :commit_message, with: 'Add a README file', visible: true
    +      # Remove pre-receive hook so we can push without auth
    +      FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
    +      click_button 'Commit Changes'
    +      visit namespace_project_tags_path(project.namespace, project)
    +    end
    +
    +    scenario 'displays a specific message' do
    +      expect(page).to have_content 'Repository has no tags yet.'
    +    end
    +  end
    +
    +  context 'when project has tags' do
    +    let(:project) { create(:project, namespace: user.namespace) }
    +    before do
    +      visit namespace_project_tags_path(project.namespace, project)
    +    end
    +
    +    scenario 'views the tags list page' do
    +      expect(page).to have_content 'v1.0.0'
    +    end
    +
    +    scenario 'views a specific tag page' do
    +      click_on 'v1.0.0'
    +
    +      expect(current_path).to eq(
    +        namespace_project_tag_path(project.namespace, project, 'v1.0.0'))
    +      expect(page).to have_content 'v1.0.0'
    +      expect(page).to have_content 'This tag has no release notes.'
    +    end
    +
    +    describe 'links on the tag page' do
    +      scenario 'has a button to browse files' do
    +        click_on 'v1.0.0'
    +
    +        expect(current_path).to eq(
    +          namespace_project_tag_path(project.namespace, project, 'v1.0.0'))
    +
    +        click_on 'Browse files'
    +
    +        expect(current_path).to eq(
    +          namespace_project_tree_path(project.namespace, project, 'v1.0.0'))
    +      end
    +
    +      scenario 'has a button to browse commits' do
    +        click_on 'v1.0.0'
    +
    +        expect(current_path).to eq(
    +          namespace_project_tag_path(project.namespace, project, 'v1.0.0'))
    +
    +        click_on 'Browse commits'
    +
    +        expect(current_path).to eq(
    +          namespace_project_commits_path(project.namespace, project, 'v1.0.0'))
    +      end
    +    end
    +  end
    +end
    diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
    index b7368cca29d..6ed279ef9be 100644
    --- a/spec/features/task_lists_spec.rb
    +++ b/spec/features/task_lists_spec.rb
    @@ -75,7 +75,10 @@ feature 'Task Lists', feature: true do
     
       describe 'for Notes' do
         let!(:issue) { create(:issue, author: user, project: project) }
    -    let!(:note)  { create(:note, note: markdown, noteable: issue, author: user) }
    +    let!(:note) do
    +      create(:note, note: markdown, noteable: issue,
    +                    project: project, author: user)
    +    end
     
         it 'renders for note body' do
           visit_issue(project, issue)
    diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/todos/target_state_spec.rb
    new file mode 100644
    index 00000000000..72491ac7e61
    --- /dev/null
    +++ b/spec/features/todos/target_state_spec.rb
    @@ -0,0 +1,65 @@
    +require 'rails_helper'
    +
    +feature 'Todo target states', feature: true do
    +  let(:user)    { create(:user) }
    +  let(:author)  { create(:user) }
    +  let(:project) { create(:project) }
    +
    +  before do
    +    login_as user
    +  end
    +
    +  scenario 'on a closed issue todo has closed label' do
    +    issue_closed = create(:issue, state: 'closed')
    +    create_todo issue_closed
    +    visit dashboard_todos_path
    +
    +    page.within '.todos-list' do
    +      expect(page).to have_content('Closed')
    +    end
    +  end
    +
    +  scenario 'on an open issue todo does not have an open label' do
    +    issue_open = create(:issue)
    +    create_todo issue_open
    +    visit dashboard_todos_path
    +
    +    page.within '.todos-list' do
    +      expect(page).not_to have_content('Open')
    +    end
    +  end
    +
    +  scenario 'on a merged merge request todo has merged label' do
    +    mr_merged = create(:merge_request, :simple, author: user, state: 'merged')
    +    create_todo mr_merged
    +    visit dashboard_todos_path
    +
    +    page.within '.todos-list' do
    +      expect(page).to have_content('Merged')
    +    end
    +  end
    +
    +  scenario 'on a closed merge request todo has closed label' do
    +    mr_closed = create(:merge_request, :simple, author: user, state: 'closed')
    +    create_todo mr_closed
    +    visit dashboard_todos_path
    +
    +    page.within '.todos-list' do
    +      expect(page).to have_content('Closed')
    +    end
    +  end
    +
    +  scenario 'on an open merge request todo does not have an open label' do
    +    mr_open = create(:merge_request, :simple, author: user)
    +    create_todo mr_open
    +    visit dashboard_todos_path
    +
    +    page.within '.todos-list' do
    +      expect(page).not_to have_content('Open')
    +    end
    +  end
    +
    +  def create_todo(target)
    +    create(:todo, :mentioned, user: user, project: project, target: target, author: author)
    +  end
    +end
    diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
    index 3354f529295..4e627753cc7 100644
    --- a/spec/features/todos/todos_spec.rb
    +++ b/spec/features/todos/todos_spec.rb
    @@ -43,6 +43,27 @@ describe 'Dashboard Todos', feature: true do
           end
         end
     
    +    context 'User has Todos with labels spanning multiple projects' do
    +      before do
    +        label1 = create(:label, project: project)
    +        note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project)
    +        create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id)
    +
    +        project2 = create(:project)
    +        label2 = create(:label, project: project2)
    +        issue2 = create(:issue, project: project2)
    +        note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2)
    +        create(:todo, :mentioned, project: project2, target: issue2, user: user, note_id: note2.id)
    +
    +        login_as(user)
    +        visit dashboard_todos_path
    +      end
    +
    +      it 'shows page with two Todos' do
    +        expect(page).to have_selector('.todos-list .todo', count: 2)
    +      end
    +    end
    +
         context 'User has multiple pages of Todos' do
           before do
             allow(Todo).to receive(:default_per_page).and_return(1)
    diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
    index afea1840cd7..a2b8f7b6931 100644
    --- a/spec/features/variables_spec.rb
    +++ b/spec/features/variables_spec.rb
    @@ -1,24 +1,53 @@
     require 'spec_helper'
     
    -describe "Variables" do
    -  let(:user) { create(:user) }
    -  before { login_as(user) }
    -
    -  describe "specific runners" do
    -    before do
    -      @project = FactoryGirl.create :empty_project
    -      @project.team << [user, :master]
    +describe 'Project variables', js: true do
    +  let(:user)     { create(:user) }
    +  let(:project)  { create(:project) }
    +  let(:variable) { create(:ci_variable, key: 'test') }
    +
    +  before do
    +    login_as(user)
    +    project.team << [user, :master]
    +    project.variables << variable
    +
    +    visit namespace_project_variables_path(project.namespace, project)
    +  end
    +
    +  it 'should show list of variables' do
    +    page.within('.variables-table') do
    +      expect(page).to have_content(variable.key)
    +    end
    +  end
    +
    +  it 'should add new variable' do
    +    fill_in('variable_key', with: 'key')
    +    fill_in('variable_value', with: 'key value')
    +    click_button('Add new variable')
    +
    +    page.within('.variables-table') do
    +      expect(page).to have_content('key')
    +    end
    +  end
    +
    +  it 'should delete variable' do
    +    page.within('.variables-table') do
    +      find('.btn-variable-delete').click
    +    end
    +
    +    expect(page).not_to have_selector('variables-table')
    +  end
    +
    +  it 'should edit variable' do
    +    page.within('.variables-table') do
    +      find('.btn-variable-edit').click
         end
     
    -    it "creates variable", js: true do
    -      visit namespace_project_variables_path(@project.namespace, @project)
    -      click_on "Add a variable"
    -      fill_in "Key", with: "SECRET_KEY"
    -      fill_in "Value", with: "SECRET_VALUE"
    -      click_on "Save changes"
    +    fill_in('variable_key', with: 'key')
    +    fill_in('variable_value', with: 'key value')
    +    click_button('Save variable')
     
    -      expect(page).to have_content("Variables were successfully updated.")
    -      expect(@project.variables.count).to eq(1)
    +    page.within('.variables-table') do
    +      expect(page).to have_content('key')
         end
       end
     end
    diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
    index bc607a29751..ec8809e6926 100644
    --- a/spec/finders/issues_finder_spec.rb
    +++ b/spec/finders/issues_finder_spec.rb
    @@ -1,10 +1,10 @@
     require 'spec_helper'
     
     describe IssuesFinder do
    -  let(:user) { create :user }
    -  let(:user2) { create :user }
    -  let(:project1) { create(:project) }
    -  let(:project2) { create(:project) }
    +  let(:user) { create(:user) }
    +  let(:user2) { create(:user) }
    +  let(:project1) { create(:empty_project) }
    +  let(:project2) { create(:empty_project) }
       let(:milestone) { create(:milestone, project: project1) }
       let(:label) { create(:label, project: project2) }
       let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
    @@ -16,101 +16,147 @@ describe IssuesFinder do
         project1.team << [user, :master]
         project2.team << [user, :developer]
         project2.team << [user2, :developer]
    +
    +    issue1
    +    issue2
    +    issue3
       end
     
    -  describe :execute do
    -    before :each do
    -      issue1
    -      issue2
    -      issue3
    -    end
    +  describe '#execute' do
    +    let(:search_user) { user }
    +    let(:params) { {} }
    +    let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute }
     
         context 'scope: all' do
    -      it 'should filter by all' do
    -        params = { scope: "all", state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues.size).to eq(3)
    +      let(:scope) { 'all' }
    +
    +      it 'returns all issues' do
    +        expect(issues).to contain_exactly(issue1, issue2, issue3)
           end
     
    -      it 'should filter by assignee id' do
    -        params = { scope: "all", assignee_id: user.id, state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues.size).to eq(2)
    +      context 'filtering by assignee ID' do
    +        let(:params) { { assignee_id: user.id } }
    +
    +        it 'returns issues assigned to that user' do
    +          expect(issues).to contain_exactly(issue1, issue2)
    +        end
           end
     
    -      it 'should filter by author id' do
    -        params = { scope: "all", author_id: user2.id, state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues).to eq([issue3])
    +      context 'filtering by author ID' do
    +        let(:params) { { author_id: user2.id } }
    +
    +        it 'returns issues created by that user' do
    +          expect(issues).to contain_exactly(issue3)
    +        end
           end
     
    -      it 'should filter by milestone id' do
    -        params = { scope: "all", milestone_title: milestone.title, state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues).to eq([issue1])
    +      context 'filtering by milestone' do
    +        let(:params) { { milestone_title: milestone.title } }
    +
    +        it 'returns issues assigned to that milestone' do
    +          expect(issues).to contain_exactly(issue1)
    +        end
           end
     
    -      it 'should filter by no milestone id' do
    -        params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues).to match_array([issue2, issue3])
    +      context 'filtering by no milestone' do
    +        let(:params) { { milestone_title: Milestone::None.title } }
    +
    +        it 'returns issues with no milestone' do
    +          expect(issues).to contain_exactly(issue2, issue3)
    +        end
           end
     
    -      it 'should filter by label name' do
    -        params = { scope: "all", label_name: label.title, state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues).to eq([issue2])
    +      context 'filtering by upcoming milestone' do
    +        let(:params) { { milestone_title: Milestone::Upcoming.name } }
    +
    +        let(:project_no_upcoming_milestones) { create(:empty_project, :public) }
    +        let(:project_next_1_1) { create(:empty_project, :public) }
    +        let(:project_next_8_8) { create(:empty_project, :public) }
    +
    +        let(:yesterday) { Date.today - 1.day }
    +        let(:tomorrow) { Date.today + 1.day }
    +        let(:two_days_from_now) { Date.today + 2.days }
    +        let(:ten_days_from_now) { Date.today + 10.days }
    +
    +        let(:milestones) do
    +          [
    +            create(:milestone, :closed, project: project_no_upcoming_milestones),
    +            create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now),
    +            create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now),
    +            create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday),
    +            create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow)
    +          ]
    +        end
    +
    +        before do
    +          milestones.each do |milestone|
    +            create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
    +          end
    +        end
    +
    +        it 'returns issues in the upcoming milestone for each project' do
    +          expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8')
    +          expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now)
    +        end
           end
     
    -      it 'returns unique issues when filtering by multiple labels' do
    -        label2 = create(:label, project: project2)
    +      context 'filtering by label' do
    +        let(:params) { { label_name: label.title } }
     
    -        create(:label_link, label: label2, target: issue2)
    +        it 'returns issues with that label' do
    +          expect(issues).to contain_exactly(issue2)
    +        end
    +      end
     
    -        params = {
    -          scope:      'all',
    -          label_name: [label.title, label2.title].join(','),
    -          state:      'opened'
    -        }
    +      context 'filtering by multiple labels' do
    +        let(:params) { { label_name: [label.title, label2.title].join(',') } }
    +        let(:label2) { create(:label, project: project2) }
     
    -        issues = IssuesFinder.new(user, params).execute
    +        before { create(:label_link, label: label2, target: issue2) }
     
    -        expect(issues).to eq([issue2])
    +        it 'returns the unique issues with any of those labels' do
    +          expect(issues).to contain_exactly(issue2)
    +        end
           end
     
    -      it 'should filter by no label name' do
    -        params = { scope: "all", label_name: Label::None.title, state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues).to match_array([issue1, issue3])
    +      context 'filtering by no label' do
    +        let(:params) { { label_name: Label::None.title } }
    +
    +        it 'returns issues with no labels' do
    +          expect(issues).to contain_exactly(issue1, issue3)
    +        end
           end
     
    -      it 'should be empty for unauthorized user' do
    -        params = { scope: "all", state: 'opened' }
    -        issues = IssuesFinder.new(nil, params).execute
    -        expect(issues.size).to be_zero
    +      context 'when the user is unauthorized' do
    +        let(:search_user) { nil }
    +
    +        it 'returns no results' do
    +          expect(issues).to be_empty
    +        end
           end
     
    -      it 'should not include unauthorized issues' do
    -        params = { scope: "all", state: 'opened' }
    -        issues = IssuesFinder.new(user2, params).execute
    -        expect(issues.size).to eq(2)
    -        expect(issues).not_to include(issue1)
    -        expect(issues).to include(issue2)
    -        expect(issues).to include(issue3)
    +      context 'when the user can see some, but not all, issues' do
    +        let(:search_user) { user2 }
    +
    +        it 'returns only issues they can see' do
    +          expect(issues).to contain_exactly(issue2, issue3)
    +        end
           end
         end
     
         context 'personal scope' do
    -      it 'should filter by assignee' do
    -        params = { scope: "assigned-to-me", state: 'opened' }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues.size).to eq(2)
    +      let(:scope) { 'assigned-to-me' }
    +
    +      it 'returns issue assigned to the user' do
    +        expect(issues).to contain_exactly(issue1, issue2)
           end
     
    -      it 'should filter by project' do
    -        params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id }
    -        issues = IssuesFinder.new(user, params).execute
    -        expect(issues.size).to eq(1)
    +      context 'filtering by project' do
    +        let(:params) { { project_id: project1.id } }
    +
    +        it 'returns issues assigned to the user in that project' do
    +          expect(issues).to contain_exactly(issue1)
    +        end
           end
         end
       end
    diff --git a/spec/fixtures/container_registry/config_blob.json b/spec/fixtures/container_registry/config_blob.json
    new file mode 100644
    index 00000000000..1028c994a24
    --- /dev/null
    +++ b/spec/fixtures/container_registry/config_blob.json
    @@ -0,0 +1 @@
    +{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}}
    diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json
    new file mode 100644
    index 00000000000..1b6008e2872
    --- /dev/null
    +++ b/spec/fixtures/container_registry/tag_manifest.json
    @@ -0,0 +1 @@
    +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]}
    diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
    index 1772cc3f6a4..34ce7c4f033 100644
    --- a/spec/fixtures/markdown.md.erb
    +++ b/spec/fixtures/markdown.md.erb
    @@ -216,10 +216,14 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
     
     #### MilestoneReferenceFilter
     
    -- Milestone: <%= milestone.to_reference %>
    +- Milestone by ID: <%= simple_milestone.to_reference %>
    +- Milestone by name: <%= Milestone.reference_prefix %><%= simple_milestone.name %>
    +- Milestone by name in quotes: <%= milestone.to_reference(format: :name) %>
     - Milestone in another project: <%= xmilestone.to_reference(project) %>
    -- Ignored in code: `<%= milestone.to_reference %>`
    -- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>)
    +- Ignored in code: `<%= simple_milestone.to_reference %>`
    +- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
    +- Milestone by URL: <%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>
    +- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
     
     ### Task Lists
     
    @@ -239,3 +243,16 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
     - [[link-text|http://example.com/pdfs/gollum.pdf]]
     - [[images/example.jpg]]
     - [[http://example.com/images/example.jpg]]
    +
    +### Inline Diffs
    +
    +With inline diffs tags you can display {+ additions +} or [- deletions -].
    +
    +The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
    +
    +However the wrapping tags can not be mixed as such -
    +
    +- {+ additions +]
    +- [+ additions +}
    +- {- delletions -]
    +- [- delletions -}
    diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
    index e47a54fdac5..49ea4fa6d3e 100644
    --- a/spec/helpers/auth_helper_spec.rb
    +++ b/spec/helpers/auth_helper_spec.rb
    @@ -2,7 +2,7 @@ require "spec_helper"
     
     describe AuthHelper do
       describe "button_based_providers" do
    -    it 'returns all enabled providers' do
    +    it 'returns all enabled providers from devise' do
           allow(helper).to receive(:auth_providers) { [:twitter, :github] }
           expect(helper.button_based_providers).to include(*[:twitter, :github])
         end
    @@ -17,4 +17,49 @@ describe AuthHelper do
           expect(helper.button_based_providers).to eq([])
         end
       end
    +
    +  describe 'enabled_button_based_providers' do
    +    before do
    +      allow(helper).to receive(:auth_providers) { [:twitter, :github] }
    +    end
    +
    +    context 'all providers are enabled to sign in' do
    +      it 'returns all the enabled providers from settings' do
    +        expect(helper.enabled_button_based_providers).to include('twitter', 'github')
    +      end
    +    end
    +
    +    context 'GitHub OAuth sign in is disabled from application setting' do
    +      it "doesn't return github as provider" do
    +        stub_application_setting(
    +          disabled_oauth_sign_in_sources: ['github']
    +        )
    +
    +        expect(helper.enabled_button_based_providers).to include('twitter')
    +        expect(helper.enabled_button_based_providers).not_to include('github')
    +      end
    +    end
    +  end
    +
    +  describe 'button_based_providers_enabled?' do
    +    before do
    +      allow(helper).to receive(:auth_providers) { [:twitter, :github] }
    +    end
    +
    +    context 'button based providers enabled' do
    +      it 'returns true' do
    +        expect(helper.button_based_providers_enabled?).to be true
    +      end
    +    end
    +
    +    context 'all the button based providers are disabled via application_setting' do
    +      it 'returns false' do
    +        stub_application_setting(
    +          disabled_oauth_sign_in_sources: ['github', 'twitter']
    +        )
    +
    +        expect(helper.button_based_providers_enabled?).to be false
    +      end
    +    end
    +  end
     end
    diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
    index b7810185d16..52764f41e0d 100644
    --- a/spec/helpers/diff_helper_spec.rb
    +++ b/spec/helpers/diff_helper_spec.rb
    @@ -93,9 +93,9 @@ describe DiffHelper do
         it "returns strings with marked inline diffs" do
           marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
     
    -      expect(marked_old_line).to eq("abc 'def'")
    +      expect(marked_old_line).to eq("abc 'def'")
           expect(marked_old_line).to be_html_safe
    -      expect(marked_new_line).to eq("abc "def"")
    +      expect(marked_new_line).to eq("abc "def"")
           expect(marked_new_line).to be_html_safe
         end
       end
    diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
    index e68a5ec29ab..c0d2be98e85 100644
    --- a/spec/helpers/events_helper_spec.rb
    +++ b/spec/helpers/events_helper_spec.rb
    @@ -1,64 +1,65 @@
     require 'spec_helper'
     
     describe EventsHelper do
    -  include ApplicationHelper
    -  include GitlabMarkdownHelper
    +  describe '#event_note' do
    +    before do
    +      allow(helper).to receive(:current_user).and_return(double)
    +    end
     
    -  let(:current_user) { create(:user, email: "current@email.com") }
    +    it 'should display one line of plain text without alteration' do
    +      input = 'A short, plain note'
    +      expect(helper.event_note(input)).to match(input)
    +      expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
    +    end
     
    -  it 'should display one line of plain text without alteration' do
    -    input = 'A short, plain note'
    -    expect(event_note(input)).to match(input)
    -    expect(event_note(input)).not_to match(/\.\.\.\z/)
    -  end
    +    it 'should display inline code' do
    +      input = 'A note with `inline code`'
    +      expected = 'A note with inline code'
     
    -  it 'should display inline code' do
    -    input = 'A note with `inline code`'
    -    expected = 'A note with inline code'
    +      expect(helper.event_note(input)).to match(expected)
    +    end
     
    -    expect(event_note(input)).to match(expected)
    -  end
    +    it 'should truncate a note with multiple paragraphs' do
    +      input = "Paragraph 1\n\nParagraph 2"
    +      expected = 'Paragraph 1...'
     
    -  it 'should truncate a note with multiple paragraphs' do
    -    input = "Paragraph 1\n\nParagraph 2"
    -    expected = 'Paragraph 1...'
    +      expect(helper.event_note(input)).to match(expected)
    +    end
     
    -    expect(event_note(input)).to match(expected)
    -  end
    +    it 'should display the first line of a code block' do
    +      input = "```\nCode block\nwith two lines\n```"
    +      expected = %r{Code block\.\.\.
    } - it 'should display the first line of a code block' do - input = "```\nCode block\nwith two lines\n```" - expected = %r{Code block\.\.\.} + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = text * 4 + expected = (text * 2).sub(/.{3}/, '...') - it 'should truncate a single long line of text' do - text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars - input = "#{text}#{text}#{text}#{text}" # 200 chars - expected = "#{text}#{text}".sub(/.{3}/, '...') + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end - - it 'should preserve a link href when link text is truncated' do - text = 'The quick brown fox jumped over the lazy dog' # 44 chars - input = "#{text}#{text}#{text} " # 133 chars - link_url = 'http://example.com/foo/bar/baz' # 30 chars - input << link_url - expected_link_text = 'http://example...' + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...' - expect(event_note(input)).to match(link_url) - expect(event_note(input)).to match(expected_link_text) - end + expect(helper.event_note(input)).to match(link_url) + expect(helper.event_note(input)).to match(expected_link_text) + end - it 'should preserve code color scheme' do - input = "```ruby\ndef test\n 'hello world'\nend\n```" - expected = '
    ' \
    -      "def test\n" \
    -      "  \'hello world\'\n" \
    -      "end" \
    -      '
    ' - expect(event_note(input)).to eq(expected) + it 'should preserve code color scheme' do + input = "```ruby\ndef test\n 'hello world'\nend\n```" + expected = '
    ' \
    +        "def test\n" \
    +        "  \'hello world\'\n" \
    +        "end" \
    +        '
    ' + expect(helper.event_note(input)).to eq(expected) + end end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 600e1c4e9ec..8e7ed42e883 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -17,7 +17,7 @@ describe MergeRequestsHelper do it 'does not include api credentials in a link' do allow(ci_service). to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c") - expect(helper.ci_build_details_path(merge_request)).to_not match("secret") + expect(helper.ci_build_details_path(merge_request)).not_to match("secret") end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 29bcb8c5892..ac5af8740dc 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -88,18 +88,18 @@ describe ProjectsHelper do end describe 'default_clone_protocol' do - describe 'using HTTP' do + context 'when user is not logged in and gitlab protocol is HTTP' do it 'returns HTTP' do - expect(helper).to receive(:current_user).and_return(nil) + allow(helper).to receive(:current_user).and_return(nil) expect(helper.send(:default_clone_protocol)).to eq('http') end end - describe 'using HTTPS' do + context 'when user is not logged in and gitlab protocol is HTTPS' do it 'returns HTTPS' do - allow(Gitlab.config.gitlab).to receive(:protocol).and_return('https') - expect(helper).to receive(:current_user).and_return(nil) + stub_config_setting(protocol: 'https') + allow(helper).to receive(:current_user).and_return(nil) expect(helper.send(:default_clone_protocol)).to eq('https') end diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml new file mode 100644 index 00000000000..95efaff4b69 --- /dev/null +++ b/spec/javascripts/fixtures/right_sidebar.html.haml @@ -0,0 +1,13 @@ +%div + %div.page-gutter.page-with-sidebar + + %aside.right-sidebar + %div.block.issuable-sidebar-header + %a.gutter-toggle.pull-right.js-sidebar-toggle + %i.fa.fa-angle-double-left + + %form.issuable-context-form + %div.block.labels + %div.sidebar-collapsed-icon + %i.fa.fa-tags + %span 1 diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js new file mode 100644 index 00000000000..82ee1954a59 --- /dev/null +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -0,0 +1,128 @@ +//= require graphs/stat_graph_contributors_graph + +describe("ContributorsGraph", function () { + describe("#set_x_domain", function () { + it("set the x_domain", function () { + ContributorsGraph.set_x_domain(20) + expect(ContributorsGraph.prototype.x_domain).toEqual(20) + }) + }) + + describe("#set_y_domain", function () { + it("sets the y_domain", function () { + ContributorsGraph.set_y_domain([{commits: 30}]) + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) + }) + }) + + describe("#init_x_domain", function () { + it("sets the initial x_domain", function () { + ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}]) + expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]) + }) + }) + + describe("#init_y_domain", function () { + it("sets the initial y_domain", function () { + ContributorsGraph.init_y_domain([{commits: 30}]) + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) + }) + }) + + describe("#init_domain", function () { + it("calls init_x_domain and init_y_domain", function () { + spyOn(ContributorsGraph, "init_x_domain") + spyOn(ContributorsGraph, "init_y_domain") + ContributorsGraph.init_domain() + expect(ContributorsGraph.init_x_domain).toHaveBeenCalled() + expect(ContributorsGraph.init_y_domain).toHaveBeenCalled() + }) + }) + + describe("#set_dates", function () { + it("sets the dates", function () { + ContributorsGraph.set_dates("2013-12-01") + expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01") + }) + }) + + describe("#set_x_domain", function () { + it("sets the instance's x domain using the prototype's x_domain", function () { + ContributorsGraph.prototype.x_domain = 20 + var instance = new ContributorsGraph() + instance.x = d3.time.scale().range([0, 100]).clamp(true) + spyOn(instance.x, 'domain') + instance.set_x_domain() + expect(instance.x.domain).toHaveBeenCalledWith(20) + }) + }) + + describe("#set_y_domain", function () { + it("sets the instance's y domain using the prototype's y_domain", function () { + ContributorsGraph.prototype.y_domain = 30 + var instance = new ContributorsGraph() + instance.y = d3.scale.linear().range([100, 0]).nice() + spyOn(instance.y, 'domain') + instance.set_y_domain() + expect(instance.y.domain).toHaveBeenCalledWith(30) + }) + }) + + describe("#set_domain", function () { + it("calls set_x_domain and set_y_domain", function () { + var instance = new ContributorsGraph() + spyOn(instance, 'set_x_domain') + spyOn(instance, 'set_y_domain') + instance.set_domain() + expect(instance.set_x_domain).toHaveBeenCalled() + expect(instance.set_y_domain).toHaveBeenCalled() + }) + }) + + describe("#set_data", function () { + it("sets the data", function () { + var instance = new ContributorsGraph() + instance.set_data("20") + expect(instance.data).toEqual("20") + }) + }) +}) + +describe("ContributorsMasterGraph", function () { + + // TODO: fix or remove + //describe("#process_dates", function () { + //it("gets and parses dates", function () { + //var graph = new ContributorsMasterGraph() + //var data = 'random data here' + //spyOn(graph, 'parse_dates') + //spyOn(graph, 'get_dates').andReturn("get") + //spyOn(ContributorsGraph,'set_dates').andCallThrough() + //graph.process_dates(data) + //expect(graph.parse_dates).toHaveBeenCalledWith(data) + //expect(graph.get_dates).toHaveBeenCalledWith(data) + //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") + //}) + //}) + + describe("#get_dates", function () { + it("plucks the date field from data collection", function () { + var graph = new ContributorsMasterGraph() + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] + expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]) + }) + }) + + describe("#parse_dates", function () { + it("parses the dates", function () { + var graph = new ContributorsMasterGraph() + var parseDate = d3.time.format("%Y-%m-%d").parse + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] + var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}] + graph.parse_dates(data) + expect(data).toEqual(correct) + }) + }) + + +}) diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js new file mode 100644 index 00000000000..5b992447473 --- /dev/null +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -0,0 +1,212 @@ +//= require graphs/stat_graph_contributors_util + +describe("ContributorsStatGraphUtil", function () { + + describe("#parse_log", function () { + it("returns a correctly parsed log", function () { + var fake_log = [ + {author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471}, + {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, + {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, + {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] + + var correct_parsed_log = { + total: [ + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author: + [ + { + author_name: "Karlo Soriano", author_email: "karlo@email.com", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author_name: "Dmitriy Zaporozhets",author_email: "dzaporozhets@email.com", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ] + } + expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log) + }) + }) + + describe("#store_data", function () { + + var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471} + var fake_total = {} + var fake_by_author = {} + + it("calls #store_commits", function () { + spyOn(ContributorsStatGraphUtil, 'store_commits') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled() + }) + + it("calls #store_additions", function () { + spyOn(ContributorsStatGraphUtil, 'store_additions') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled() + }) + + it("calls #store_deletions", function () { + spyOn(ContributorsStatGraphUtil, 'store_deletions') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled() + }) + + }) + + // TODO: fix or remove + //describe("#store_commits", function () { + //var fake_total = "fake_total" + //var fake_by_author = "fake_by_author" + + //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + //spyOn(ContributorsStatGraphUtil, 'add') + //ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author) + //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]) + //}) + //}) + + describe("#add", function () { + it("adds 1 to current test_field in collection", function () { + var fake_collection = {test_field: 10} + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) + expect(fake_collection.test_field).toEqual(11) + }) + + it("inits and adds 1 if test_field in collection is not defined", function () { + var fake_collection = {} + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) + expect(fake_collection.test_field).toEqual(1) + }) + }) + + // TODO: fix or remove + //describe("#store_additions", function () { + //var fake_entry = {additions: 10} + //var fake_total= "fake_total" + //var fake_by_author = "fake_by_author" + //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + //spyOn(ContributorsStatGraphUtil, 'add') + //ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author) + //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]) + //}) + //}) + + // TODO: fix or remove + //describe("#store_deletions", function () { + //var fake_entry = {deletions: 10} + //var fake_total= "fake_total" + //var fake_by_author = "fake_by_author" + //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + //spyOn(ContributorsStatGraphUtil, 'add') + //ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author) + //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]) + //}) + //}) + + describe("#add_date", function () { + it("adds a date field to the collection", function () { + var fake_date = "2013-10-02" + var fake_collection = {} + ContributorsStatGraphUtil.add_date(fake_date, fake_collection) + expect(fake_collection[fake_date].date).toEqual("2013-10-02") + }) + }) + + describe("#add_author", function () { + it("adds an author field to the collection", function () { + var fake_author = { author_name: "Author", author_email: 'fake@email.com' } + var fake_author_collection = {} + var fake_email_collection = {} + ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection) + expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author") + expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author") + }) + }) + + describe("#get_total_data", function () { + it("returns the collection sorted via specified field", function () { + var fake_parsed_log = { + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author:[ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ]}; + var correct_total_data = [{date: "2013-05-08", commits: 3}, + {date: "2013-05-09", commits: 1}]; + expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data) + }) + }) + + describe("#pick_field", function () { + it("returns the collection with only the specified field and date", function () { + var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}]; + ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits") + var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}]; + expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data) + }) + }) + + describe("#get_author_data", function () { + it("returns the log by author sorted by specified field", function () { + var fake_parsed_log = { + total: [ + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + ], + by_author: [ + { + author_name: "Karlo Soriano", author_email: "karlo@email.com", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ] + } + var correct_author_data = [ + {author_name:"Dmitriy Zaporozhets",author_email:"dzaporozhets@email.com",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3}, + {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} + ] + expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data) + }) + }) + + describe("#parse_log_entry", function () { + it("adds the corresponding info from the log entry to the author", function () { + var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + } + var correct_parsed_log = {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} + expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log) + }) + }) + + describe("#in_range", function () { + var date = "2013-05-09" + it("returns true if date_range is null", function () { + expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true) + }) + it("returns true if date is in range", function () { + var date_range = [new Date("2013-01-01"), new Date("2013-12-12")] + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true) + }) + it("returns false if date is not in range", function () { + var date_range = [new Date("1999-12-01"), new Date("2000-12-01")] + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false) + }) + }) + + +}) diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js new file mode 100644 index 00000000000..4b05d401a42 --- /dev/null +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -0,0 +1,19 @@ +//= require graphs/stat_graph + +describe("StatGraph", function () { + + describe("#get_log", function () { + it("returns log", function () { + StatGraph.log = "test"; + expect(StatGraph.get_log()).toBe("test"); + }); + }); + + describe("#set_log", function () { + it("sets the log", function () { + StatGraph.set_log("test"); + expect(StatGraph.log).toBe("test"); + }) + }) + +}); diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 3d8de2ff989..1cf34d4d2d3 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,5 +1,6 @@ #= require bootstrap #= require select2 +#= require lib/type_utility #= require gl_dropdown #= require api #= require project_select diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee new file mode 100644 index 00000000000..2075cacdb67 --- /dev/null +++ b/spec/javascripts/right_sidebar_spec.js.coffee @@ -0,0 +1,69 @@ +#= require right_sidebar +#= require jquery +#= require jquery.cookie + +@sidebar = null +$aside = null +$toggle = null +$icon = null +$page = null +$labelsIcon = null + + +assertSidebarState = (state) -> + + shouldBeExpanded = state is 'expanded' + shouldBeCollapsed = state is 'collapsed' + + expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded + expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded + expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded + + expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed + expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed + expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed + + +describe 'RightSidebar', -> + + fixture.preload 'right_sidebar.html' + + beforeEach -> + fixture.load 'right_sidebar.html' + + @sidebar = new Sidebar + $aside = $ '.right-sidebar' + $page = $ '.page-with-sidebar' + $icon = $aside.find 'i' + $toggle = $aside.find '.js-sidebar-toggle' + $labelsIcon = $aside.find '.sidebar-collapsed-icon' + + + it 'should expand the sidebar when arrow is clicked', -> + + $toggle.click() + assertSidebarState 'expanded' + + + it 'should collapse the sidebar when arrow is clicked', -> + + $toggle.click() + assertSidebarState 'expanded' + + $toggle.click() + assertSidebarState 'collapsed' + + + it 'should float over the page and when sidebar icons clicked', -> + + $labelsIcon.click() + assertSidebarState 'expanded' + + + it 'should collapse when the icon arrow clicked while it is floating on page', -> + + $labelsIcon.click() + assertSidebarState 'expanded' + + $toggle.click() + assertSidebarState 'collapsed' diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js deleted file mode 100644 index 78d39f1b428..00000000000 --- a/spec/javascripts/stat_graph_contributors_graph_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -//= require stat_graph_contributors_graph - -describe("ContributorsGraph", function () { - describe("#set_x_domain", function () { - it("set the x_domain", function () { - ContributorsGraph.set_x_domain(20) - expect(ContributorsGraph.prototype.x_domain).toEqual(20) - }) - }) - - describe("#set_y_domain", function () { - it("sets the y_domain", function () { - ContributorsGraph.set_y_domain([{commits: 30}]) - expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) - }) - }) - - describe("#init_x_domain", function () { - it("sets the initial x_domain", function () { - ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}]) - expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]) - }) - }) - - describe("#init_y_domain", function () { - it("sets the initial y_domain", function () { - ContributorsGraph.init_y_domain([{commits: 30}]) - expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) - }) - }) - - describe("#init_domain", function () { - it("calls init_x_domain and init_y_domain", function () { - spyOn(ContributorsGraph, "init_x_domain") - spyOn(ContributorsGraph, "init_y_domain") - ContributorsGraph.init_domain() - expect(ContributorsGraph.init_x_domain).toHaveBeenCalled() - expect(ContributorsGraph.init_y_domain).toHaveBeenCalled() - }) - }) - - describe("#set_dates", function () { - it("sets the dates", function () { - ContributorsGraph.set_dates("2013-12-01") - expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01") - }) - }) - - describe("#set_x_domain", function () { - it("sets the instance's x domain using the prototype's x_domain", function () { - ContributorsGraph.prototype.x_domain = 20 - var instance = new ContributorsGraph() - instance.x = d3.time.scale().range([0, 100]).clamp(true) - spyOn(instance.x, 'domain') - instance.set_x_domain() - expect(instance.x.domain).toHaveBeenCalledWith(20) - }) - }) - - describe("#set_y_domain", function () { - it("sets the instance's y domain using the prototype's y_domain", function () { - ContributorsGraph.prototype.y_domain = 30 - var instance = new ContributorsGraph() - instance.y = d3.scale.linear().range([100, 0]).nice() - spyOn(instance.y, 'domain') - instance.set_y_domain() - expect(instance.y.domain).toHaveBeenCalledWith(30) - }) - }) - - describe("#set_domain", function () { - it("calls set_x_domain and set_y_domain", function () { - var instance = new ContributorsGraph() - spyOn(instance, 'set_x_domain') - spyOn(instance, 'set_y_domain') - instance.set_domain() - expect(instance.set_x_domain).toHaveBeenCalled() - expect(instance.set_y_domain).toHaveBeenCalled() - }) - }) - - describe("#set_data", function () { - it("sets the data", function () { - var instance = new ContributorsGraph() - instance.set_data("20") - expect(instance.data).toEqual("20") - }) - }) -}) - -describe("ContributorsMasterGraph", function () { - - // TODO: fix or remove - //describe("#process_dates", function () { - //it("gets and parses dates", function () { - //var graph = new ContributorsMasterGraph() - //var data = 'random data here' - //spyOn(graph, 'parse_dates') - //spyOn(graph, 'get_dates').andReturn("get") - //spyOn(ContributorsGraph,'set_dates').andCallThrough() - //graph.process_dates(data) - //expect(graph.parse_dates).toHaveBeenCalledWith(data) - //expect(graph.get_dates).toHaveBeenCalledWith(data) - //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") - //}) - //}) - - describe("#get_dates", function () { - it("plucks the date field from data collection", function () { - var graph = new ContributorsMasterGraph() - var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] - expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]) - }) - }) - - describe("#parse_dates", function () { - it("parses the dates", function () { - var graph = new ContributorsMasterGraph() - var parseDate = d3.time.format("%Y-%m-%d").parse - var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] - var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}] - graph.parse_dates(data) - expect(data).toEqual(correct) - }) - }) - - -}) diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js deleted file mode 100644 index dbafe782b77..00000000000 --- a/spec/javascripts/stat_graph_contributors_util_spec.js +++ /dev/null @@ -1,212 +0,0 @@ -//= require stat_graph_contributors_util - -describe("ContributorsStatGraphUtil", function () { - - describe("#parse_log", function () { - it("returns a correctly parsed log", function () { - var fake_log = [ - {author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471}, - {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, - {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, - {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] - - var correct_parsed_log = { - total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], - by_author: - [ - { - author_name: "Karlo Soriano", author_email: "karlo@email.com", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - }, - { - author_name: "Dmitriy Zaporozhets",author_email: "dzaporozhets@email.com", - "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} - } - ] - } - expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log) - }) - }) - - describe("#store_data", function () { - - var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471} - var fake_total = {} - var fake_by_author = {} - - it("calls #store_commits", function () { - spyOn(ContributorsStatGraphUtil, 'store_commits') - ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) - expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled() - }) - - it("calls #store_additions", function () { - spyOn(ContributorsStatGraphUtil, 'store_additions') - ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) - expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled() - }) - - it("calls #store_deletions", function () { - spyOn(ContributorsStatGraphUtil, 'store_deletions') - ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) - expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled() - }) - - }) - - // TODO: fix or remove - //describe("#store_commits", function () { - //var fake_total = "fake_total" - //var fake_by_author = "fake_by_author" - - //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { - //spyOn(ContributorsStatGraphUtil, 'add') - //ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author) - //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]) - //}) - //}) - - describe("#add", function () { - it("adds 1 to current test_field in collection", function () { - var fake_collection = {test_field: 10} - ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) - expect(fake_collection.test_field).toEqual(11) - }) - - it("inits and adds 1 if test_field in collection is not defined", function () { - var fake_collection = {} - ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) - expect(fake_collection.test_field).toEqual(1) - }) - }) - - // TODO: fix or remove - //describe("#store_additions", function () { - //var fake_entry = {additions: 10} - //var fake_total= "fake_total" - //var fake_by_author = "fake_by_author" - //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { - //spyOn(ContributorsStatGraphUtil, 'add') - //ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author) - //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]) - //}) - //}) - - // TODO: fix or remove - //describe("#store_deletions", function () { - //var fake_entry = {deletions: 10} - //var fake_total= "fake_total" - //var fake_by_author = "fake_by_author" - //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { - //spyOn(ContributorsStatGraphUtil, 'add') - //ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author) - //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]) - //}) - //}) - - describe("#add_date", function () { - it("adds a date field to the collection", function () { - var fake_date = "2013-10-02" - var fake_collection = {} - ContributorsStatGraphUtil.add_date(fake_date, fake_collection) - expect(fake_collection[fake_date].date).toEqual("2013-10-02") - }) - }) - - describe("#add_author", function () { - it("adds an author field to the collection", function () { - var fake_author = { author_name: "Author", author_email: 'fake@email.com' } - var fake_author_collection = {} - var fake_email_collection = {} - ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection) - expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author") - expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author") - }) - }) - - describe("#get_total_data", function () { - it("returns the collection sorted via specified field", function () { - var fake_parsed_log = { - total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], - by_author:[ - { - author: "Karlo Soriano", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - }, - { - author: "Dmitriy Zaporozhets", - "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} - } - ]}; - var correct_total_data = [{date: "2013-05-08", commits: 3}, - {date: "2013-05-09", commits: 1}]; - expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data) - }) - }) - - describe("#pick_field", function () { - it("returns the collection with only the specified field and date", function () { - var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}]; - ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits") - var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}]; - expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data) - }) - }) - - describe("#get_author_data", function () { - it("returns the log by author sorted by specified field", function () { - var fake_parsed_log = { - total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} - ], - by_author: [ - { - author_name: "Karlo Soriano", author_email: "karlo@email.com", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - }, - { - author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", - "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} - } - ] - } - var correct_author_data = [ - {author_name:"Dmitriy Zaporozhets",author_email:"dzaporozhets@email.com",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3}, - {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} - ] - expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data) - }) - }) - - describe("#parse_log_entry", function () { - it("adds the corresponding info from the log entry to the author", function () { - var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - } - var correct_parsed_log = {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} - expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log) - }) - }) - - describe("#in_range", function () { - var date = "2013-05-09" - it("returns true if date_range is null", function () { - expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true) - }) - it("returns true if date is in range", function () { - var date_range = [new Date("2013-01-01"), new Date("2013-12-12")] - expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true) - }) - it("returns false if date is not in range", function () { - var date_range = [new Date("1999-12-01"), new Date("2000-12-01")] - expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false) - }) - }) - - -}) diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/stat_graph_spec.js deleted file mode 100644 index 4c652910cd6..00000000000 --- a/spec/javascripts/stat_graph_spec.js +++ /dev/null @@ -1,19 +0,0 @@ -//= require stat_graph - -describe("StatGraph", function () { - - describe("#get_log", function () { - it("returns log", function () { - StatGraph.log = "test"; - expect(StatGraph.get_log()).toBe("test"); - }); - }); - - describe("#set_log", function () { - it("sets the log", function () { - StatGraph.set_log("test"); - expect(StatGraph.log).toBe("test"); - }) - }) - -}); diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb index 88c22912950..c3098574292 100644 --- a/spec/lib/award_emoji_spec.rb +++ b/spec/lib/award_emoji_spec.rb @@ -5,7 +5,7 @@ describe AwardEmoji do subject { AwardEmoji.urls } it { is_expected.to be_an_instance_of(Array) } - it { is_expected.to_not be_empty } + it { is_expected.not_to be_empty } context 'every Hash in the Array' do it 'has the correct keys and values' do diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index c2a8ad36c30..593bd6d5cac 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end end context 'cross-project reference' do @@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end end context 'cross-project URL reference' do @@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end end end diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 63a32d9d455..d46d3f1489e 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end end context 'cross-project reference' do @@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do exp = act = "Committed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end end context 'cross-project URL reference' do @@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do act = "Committed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to match(/#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end end end diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb new file mode 100644 index 00000000000..9e526371294 --- /dev/null +++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Banzai::Filter::InlineDiffFilter, lib: true do + include FilterSpecHelper + + it 'adds inline diff span tags for deletions when using square brackets' do + doc = "START [-something deleted-] END" + expect(filter(doc).to_html).to eq('START something deleted END') + end + + it 'adds inline diff span tags for deletions when using curley braces' do + doc = "START {-something deleted-} END" + expect(filter(doc).to_html).to eq('START something deleted END') + end + + it 'does not add inline diff span tags when a closing tag is not provided' do + doc = "START [- END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'adds inline span tags for additions when using square brackets' do + doc = "START [+something added+] END" + expect(filter(doc).to_html).to eq('START something added END') + end + + it 'adds inline span tags for additions when using curley braces' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq('START something added END') + end + + it 'does not add inline diff span tags when a closing addition tag is not provided' do + doc = "START {+ END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'does not add inline diff span tags when the tags do not match' do + examples = [ + "{+ additions +]", + "[+ additions +}", + "{- delletions -]", + "[- delletions -}" + ] + + examples.each do |doc| + expect(filter(doc).to_html).to eq(doc) + end + end + + it 'prevents user-land html being injected' do + doc = "START {+<script>alert('I steal cookies')</script>+} END" + expect(filter(doc).to_html).to eq("START <script>alert('I steal cookies')</script> END") + end + + it 'preserves content inside pre tags' do + doc = "
    START {+something added+} END
    " + expect(filter(doc).to_html).to eq(doc) + end + + it 'preserves content inside code tags' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'preserves content inside tt tags' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq(doc) + end +end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 266ebef33d6..8e6a264970d 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) end - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - it 'does not process links containing issue numbers followed by text' do href = "#{reference}st" doc = reference_filter("") @@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end context 'cross-project URL reference' do @@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end context 'cross-project reference in link href' do @@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(Reference<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end context 'cross-project URL in link href' do @@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(Reference<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index b0a38e7c251..f1064a701d8 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end - describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") @@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to have_attribute('data-label') expect(link.attr('data-label')).to eq label.id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end end describe 'cross project label references' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 352710df307..3185e41fe5c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end end context 'cross-project reference' do @@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end end context 'cross-project URL reference' do @@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do doc = reference_filter("Merge (#{reference}.)") expect(doc.to_html).to match(/\(#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ebf3d7489b5..9424f2363e1 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe Banzai::Filter::MilestoneReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:reference) { milestone.to_reference } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -17,11 +18,37 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end end - context 'internal reference' do - # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline. - # Milestone reference behavior in the full Markdown pipeline is tested elsewhere. - let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') } + it 'includes default classes' do + doc = reference_filter("Milestone #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Milestone #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Milestone #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + expect(link).not_to match %r(https?://) + expect(link).to eq urls. + namespace_project_milestone_path(project.namespace, project, milestone) + end + + context 'Integer-based references' do it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -30,29 +57,82 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end it 'links with adjacent text' do - doc = reference_filter("milestone (#{reference}.)") - expect(doc.to_html).to match(/\(#{Regexp.escape(milestone.title)}<\/a>\.\)/) + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) end - it 'includes a title attribute' do - doc = reference_filter("milestone #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}" + it 'ignores invalid milestone IIDs' do + exp = act = "Milestone #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end + end + + context 'String-based single-word references' do + let(:milestone) { create(:milestone, name: 'gfm', project: project) } + let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") - it 'escapes the title attribute' do - milestone.update_attribute(:title, %{">whatever#{milestone.name}\.\))) end - it 'includes default classes' do - doc = reference_filter("milestone #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + it 'ignores invalid milestone names' do + exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:milestone) { create(:milestone, name: 'gfm references', project: project) } + let(:reference) { milestone.to_reference(format: :name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone names' do + exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'referencing a milestone in a link href' do + let(:reference) { %Q{Milestone} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(Milestone\.\))) end it 'includes a data-project attribute' do - doc = reference_filter("milestone #{reference}") + doc = reference_filter("Milestone #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -66,10 +146,31 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do expect(link).to have_attribute('data-milestone') expect(link.attr('data-milestone')).to eq milestone.id.to_s end + end + + describe 'cross project milestone references' do + let(:another_project) { create(:empty_project, :public) } + let(:project_path) { another_project.path_with_namespace } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { milestone.to_reference(project) } + + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end - it 'adds to the results hash' do - result = reference_pipeline_result("milestone #{reference}") - expect(result[:references][:milestone]).to eq [milestone] + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{">whateverText) + exp = 'Text' + expect(filter(act).to_html).to eq exp + end + it 'allows whitelisted HTML tags from the user' do exp = act = "
    \n
    Term
    \n
    Definition
    \n
    " expect(filter(act).to_html).to eq exp diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 26466fbb180..5068ddd7faa 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end end context 'cross-project reference' do @@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end end context 'cross-project URL reference' do @@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do expect(reference_filter(act).to_html).to match(/#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end end end diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 3b073a90a95..b83be54746c 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -8,6 +8,10 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do project: project }) + raw_filter(doc, contexts) + end + + def raw_filter(doc, contexts = {}) described_class.call(doc, contexts) end @@ -70,4 +74,18 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" end end + + context 'when project context does not exist' do + let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } + + it 'does not raise error' do + expect { raw_filter(upload_link, project: nil) }.not_to raise_error + end + + it 'does not rewrite link' do + doc = raw_filter(upload_link, project: nil) + + expect(doc.to_html).to eq upload_link + end + end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 8bdebae1841..d7dfd6699ef 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end it 'supports a special @all mention' do - doc = reference_filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}", author: user) expect(doc.css('a').length).to eq 1 expect(doc.css('a').first.attr('href')) .to eq urls.namespace_project_url(project.namespace, project) end - context "when the author is a member of the project" do + it 'includes a data-author attribute when there is an author' do + doc = reference_filter(reference, author: user) - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}", author: project.creator) - expect(result[:references][:user]).to eq [project.creator] - end + expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s) end - context "when the author is not a member of the project" do - - let(:other_user) { create(:user) } + it 'does not include a data-author attribute when there is no author' do + doc = reference_filter(reference) - it "doesn't add to the results hash" do - result = reference_pipeline_result("Hey #{reference}", author: other_user) - expect(result[:references][:user]).to eq [] - end + expect(doc.css('a').first.has_attribute?('data-author')).to eq(false) end end @@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link).to have_attribute('data-user') expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] - end end context 'mentioning a group' do @@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link).to have_attribute('data-group') expect(link.attr('data-group')).to eq group.id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq group.users - end end it 'links with adjacent text' do @@ -151,10 +135,5 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link).to have_attribute('data-user') expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] - end end end diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb new file mode 100644 index 00000000000..185abbb2108 --- /dev/null +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Banzai::Filter::WikiLinkFilter, lib: true do + include FilterSpecHelper + + let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") } + let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:user) { double } + let(:project_wiki) { ProjectWiki.new(project, user) } + + describe "links within the wiki (relative)" do + describe "hierarchical links to the current directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('./page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('./page.md') + end + end + + describe "hierarchical links to the parent directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('../page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('../page.md') + end + end + + describe "hierarchical links to a sub-directory" do + it "doesn't rewrite non-file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md') + end + end + + describe "non-hierarchical links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to match('/wiki_link_ns/wiki_link_project/wikis/page') + end + + it "doesn't rewrite file links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('page.md') + end + end + end + + describe "links outside the wiki (absolute)" do + it "doesn't rewrite links" do + link = "Link to Page" + filtered_link = filter(link, project_wiki: project_wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('http://example.com/page') + end + end +end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb new file mode 100644 index 00000000000..543b4786d84 --- /dev/null +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -0,0 +1,237 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::BaseParser, lib: true do + include ReferenceParserHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + subject do + klass = Class.new(described_class) do + self.reference_type = :foo + end + + klass.new(project, user) + end + + describe '.reference_type=' do + it 'sets the reference type' do + dummy = Class.new(described_class) + dummy.reference_type = :foo + + expect(dummy.reference_type).to eq(:foo) + end + end + + describe '#nodes_visible_to_user' do + let(:link) { empty_html_link } + + context 'when the link has a data-project attribute' do + it 'returns the nodes if the attribute value equals the current project ID' do + link['data-project'] = project.id.to_s + + expect(Ability.abilities).not_to receive(:allowed?) + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns the nodes if the user can read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the attribute value is empty' do + link['data-project'] = '' + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + + it 'returns an empty Array when the user can not read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns the nodes' do + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + end + + describe '#nodes_user_can_reference' do + it 'returns the nodes' do + link = double(:link) + + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end + end + + describe '#referenced_by' do + context 'when references_relation is implemented' do + it 'returns a collection of objects' do + links = Nokogiri::HTML.fragment(""). + children + + expect(subject).to receive(:references_relation).and_return(User) + expect(subject.referenced_by(links)).to eq([user]) + end + end + + context 'when references_relation is not implemented' do + it 'raises NotImplementedError' do + links = Nokogiri::HTML.fragment('').children + + expect { subject.referenced_by(links) }. + to raise_error(NotImplementedError) + end + end + end + + describe '#references_relation' do + it 'raises NotImplementedError' do + expect { subject.references_relation }.to raise_error(NotImplementedError) + end + end + + describe '#gather_attributes_per_project' do + it 'returns a Hash containing attribute values per project' do + link = Nokogiri::HTML.fragment(''). + children[0] + + hash = subject.gather_attributes_per_project([link], 'data-foo') + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[1].to_a).to eq(['2']) + end + end + + describe '#grouped_objects_for_nodes' do + it 'returns a Hash grouping objects per ID' do + nodes = [double(:node)] + + expect(subject).to receive(:unique_attribute_values). + with(nodes, 'data-user'). + and_return([user.id]) + + hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') + + expect(hash).to eq({ user.id => user }) + end + + it 'returns an empty Hash when the list of nodes is empty' do + expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({}) + end + end + + describe '#unique_attribute_values' do + it 'returns an Array of unique values' do + link = double(:link) + + expect(link).to receive(:has_attribute?). + with('data-foo'). + twice. + and_return(true) + + expect(link).to receive(:attr). + with('data-foo'). + twice. + and_return('1') + + nodes = [link, link] + + expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1']) + end + end + + describe '#process' do + it 'gathers the references for every node matching the reference type' do + dummy = Class.new(described_class) do + self.reference_type = :test + end + + instance = dummy.new(project, user) + document = Nokogiri::HTML.fragment('') + + expect(instance).to receive(:gather_references). + with([document.children[1]]). + and_return([user]) + + expect(instance.process([document])).to eq([user]) + end + end + + describe '#gather_references' do + let(:link) { double(:link) } + + it 'does not process links a user can not reference' do + expect(subject).to receive(:nodes_user_can_reference). + with(user, [link]). + and_return([]) + + expect(subject).to receive(:referenced_by).with([]) + + subject.gather_references([link]) + end + + it 'does not process links a user can not see' do + expect(subject).to receive(:nodes_user_can_reference). + with(user, [link]). + and_return([link]) + + expect(subject).to receive(:nodes_visible_to_user). + with(user, [link]). + and_return([]) + + expect(subject).to receive(:referenced_by).with([]) + + subject.gather_references([link]) + end + + it 'returns the references if a user can reference and see a link' do + expect(subject).to receive(:nodes_user_can_reference). + with(user, [link]). + and_return([link]) + + expect(subject).to receive(:nodes_visible_to_user). + with(user, [link]). + and_return([link]) + + expect(subject).to receive(:referenced_by).with([link]) + + subject.gather_references([link]) + end + end + + describe '#can?' do + it 'delegates the permissions check to the Ability class' do + user = double(:user) + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, project) + + subject.can?(user, :read_project, project) + end + end + + describe '#find_projects_for_hash_keys' do + it 'returns a list of Projects' do + expect(subject.find_projects_for_hash_keys(project.id => project)). + to eq([project]) + end + end +end diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb new file mode 100644 index 00000000000..0b76d29fce0 --- /dev/null +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::CommitParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-project attribute' do + before do + link['data-project'] = project.id.to_s + end + + context 'when the link has a data-commit attribute' do + before do + link['data-commit'] = '123' + end + + it 'returns an Array of commits' do + commit = double(:commit) + + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject).to receive(:find_commits). + with(project, ['123']). + and_return([commit]) + + expect(subject.referenced_by([link])).to eq([commit]) + end + + it 'returns an empty Array when the commit could not be found' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject).to receive(:find_commits). + with(project, ['123']). + and_return([]) + + expect(subject.referenced_by([link])).to eq([]) + end + + it 'skips projects without valid repositories' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(false) + + expect(subject.referenced_by([link])).to eq([]) + end + end + + context 'when the link does not have a data-commit attribute' do + it 'returns an empty Array' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns an empty Array' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + describe '#commit_ids_per_project' do + before do + link['data-project'] = project.id.to_s + end + + it 'returns a Hash containing commit IDs per project' do + link['data-commit'] = '123' + + hash = subject.commit_ids_per_project([link]) + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[project.id].to_a).to eq(['123']) + end + + it 'does not add a project when the data-commit attribute is empty' do + hash = subject.commit_ids_per_project([link]) + + expect(hash).to be_empty + end + end + + describe '#find_commits' do + it 'returns an Array of commit objects' do + commit = double(:commit) + + expect(project).to receive(:commit).with('123').and_return(commit) + expect(project).to receive(:valid_repo?).and_return(true) + + expect(subject.find_commits(project, %w{123})).to eq([commit]) + end + + it 'skips commit IDs for which no commit could be found' do + expect(project).to receive(:commit).with('123').and_return(nil) + expect(project).to receive(:valid_repo?).and_return(true) + + expect(subject.find_commits(project, %w{123})).to eq([]) + end + end +end diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb new file mode 100644 index 00000000000..ba982f38542 --- /dev/null +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::CommitRangeParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-project attribute' do + before do + link['data-project'] = project.id.to_s + end + + context 'when the link as a data-commit-range attribute' do + before do + link['data-commit-range'] = '123..456' + end + + it 'returns an Array of commit ranges' do + range = double(:range) + + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(range) + + expect(subject.referenced_by([link])).to eq([range]) + end + + it 'returns an empty Array when the commit range could not be found' do + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(nil) + + expect(subject.referenced_by([link])).to eq([]) + end + end + + context 'when the link does not have a data-commit-range attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + describe '#commit_range_ids_per_project' do + before do + link['data-project'] = project.id.to_s + end + + it 'returns a Hash containing range IDs per project' do + link['data-commit-range'] = '123..456' + + hash = subject.commit_range_ids_per_project([link]) + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[project.id].to_a).to eq(['123..456']) + end + + it 'does not add a project when the data-commit-range attribute is empty' do + hash = subject.commit_range_ids_per_project([link]) + + expect(hash).to be_empty + end + end + + describe '#find_ranges' do + it 'returns an Array of range objects' do + range = double(:commit) + + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(range) + + expect(subject.find_ranges(project, ['123..456'])).to eq([range]) + end + + it 'skips ranges that could not be found' do + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(nil) + + expect(subject.find_ranges(project, ['123..456'])).to eq([]) + end + end + + describe '#find_object' do + let(:range) { double(:range) } + + before do + expect(CommitRange).to receive(:new).and_return(range) + end + + context 'when the range has valid commits' do + it 'returns the commit range' do + expect(range).to receive(:valid_commits?).and_return(true) + + expect(subject.find_object(project, '123..456')).to eq(range) + end + end + + context 'when the range does not have any valid commits' do + it 'returns nil' do + expect(range).to receive(:valid_commits?).and_return(false) + + expect(subject.find_object(project, '123..456')).to be_nil + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb new file mode 100644 index 00000000000..a6ef8394fe7 --- /dev/null +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-project attribute' do + before do + link['data-project'] = project.id.to_s + end + + context 'when the link has a data-external-issue attribute' do + it 'returns an Array of ExternalIssue instances' do + link['data-external-issue'] = '123' + + refs = subject.referenced_by([link]) + + expect(refs).to eq([ExternalIssue.new('123', project)]) + end + end + + context 'when the link does not have a data-external-issue attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + describe '#issue_ids_per_project' do + before do + link['data-project'] = project.id.to_s + end + + it 'returns a Hash containing range IDs per project' do + link['data-external-issue'] = '123' + + hash = subject.issue_ids_per_project([link]) + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[project.id].to_a).to eq(['123']) + end + + it 'does not add a project when the data-external-issue attribute is empty' do + hash = subject.issue_ids_per_project([link]) + + expect(hash).to be_empty + end + end +end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb new file mode 100644 index 00000000000..514c752546d --- /dev/null +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::IssueParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before do + link['data-issue'] = issue.id.to_s + end + + it 'returns the nodes when the user can read the issue' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_issue, issue). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the user can not read the issue' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_issue, issue). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-issue attribute' do + it 'returns an empty Array' do + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the project uses an external issue tracker' do + it 'returns all nodes' do + link = double(:link) + + expect(project).to receive(:external_issue_tracker).and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + end + + describe '#referenced_by' do + context 'when the link has a data-issue attribute' do + context 'using an existing issue ID' do + before do + link['data-issue'] = issue.id.to_s + end + + it 'returns an Array of issues' do + expect(subject.referenced_by([link])).to eq([issue]) + end + + it 'returns an empty Array when the list of nodes is empty' do + expect(subject.referenced_by([link])).to eq([issue]) + expect(subject.referenced_by([])).to eq([]) + end + end + end + end + + describe '#issues_for_nodes' do + it 'returns a Hash containing the issues for a list of nodes' do + link['data-issue'] = issue.id.to_s + nodes = [link] + + expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue }) + end + end +end diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb new file mode 100644 index 00000000000..77fda47f0e7 --- /dev/null +++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::LabelParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:label) { create(:label, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-label attribute' do + context 'using an existing label ID' do + it 'returns an Array of labels' do + link['data-label'] = label.id.to_s + + expect(subject.referenced_by([link])).to eq([label]) + end + end + + context 'using a non-existing label ID' do + it 'returns an empty Array' do + link['data-label'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb new file mode 100644 index 00000000000..cf89ad598ea --- /dev/null +++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::MergeRequestParser, lib: true do + include ReferenceParserHelpers + + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + subject { described_class.new(merge_request.target_project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-merge-request attribute' do + context 'using an existing merge request ID' do + it 'returns an Array of merge requests' do + link['data-merge-request'] = merge_request.id.to_s + + expect(subject.referenced_by([link])).to eq([merge_request]) + end + end + + context 'using a non-existing merge request ID' do + it 'returns an empty Array' do + link['data-merge-request'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb new file mode 100644 index 00000000000..6aa45a22cc4 --- /dev/null +++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::MilestoneParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:milestone) { create(:milestone, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-milestone attribute' do + context 'using an existing milestone ID' do + it 'returns an Array of milestones' do + link['data-milestone'] = milestone.id.to_s + + expect(subject.referenced_by([link])).to eq([milestone]) + end + end + + context 'using a non-existing milestone ID' do + it 'returns an empty Array' do + link['data-milestone'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb new file mode 100644 index 00000000000..59127b7c5d1 --- /dev/null +++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::SnippetParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:snippet) { create(:snippet, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-snippet attribute' do + context 'using an existing snippet ID' do + it 'returns an Array of snippets' do + link['data-snippet'] = snippet.id.to_s + + expect(subject.referenced_by([link])).to eq([snippet]) + end + end + + context 'using a non-existing snippet ID' do + it 'returns an empty Array' do + link['data-snippet'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb new file mode 100644 index 00000000000..9a82891297d --- /dev/null +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::UserParser, lib: true do + include ReferenceParserHelpers + + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public, group: group, creator: user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-group attribute' do + context 'using an existing group ID' do + before do + link['data-group'] = project.group.id.to_s + end + + it 'returns the users of the group' do + create(:group_member, group: group, user: user) + + expect(subject.referenced_by([link])).to eq([user]) + end + + it 'returns an empty Array when the group has no users' do + expect(subject.referenced_by([link])).to eq([]) + end + end + + context 'using a non-existing group ID' do + it 'returns an empty Array' do + link['data-group'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link has a data-user attribute' do + it 'returns an Array of users' do + link['data-user'] = user.id.to_s + + expect(subject.referenced_by([link])).to eq([user]) + end + end + + context 'when the link has a data-project attribute' do + context 'using an existing project ID' do + let(:contributor) { create(:user) } + + before do + project.team << [user, :developer] + project.team << [contributor, :developer] + end + + it 'returns the members of a project' do + link['data-project'] = project.id.to_s + + # This uses an explicit sort to make sure this spec doesn't randomly + # fail when objects are returned in a different order. + refs = subject.referenced_by([link]).sort_by(&:id) + + expect(refs).to eq([user, contributor]) + end + end + + context 'using a non-existing project ID' do + it 'returns an empty Array' do + link['data-project'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end + + describe '#nodes_visible_to_use?' do + context 'when the link has a data-group attribute' do + context 'using an existing group ID' do + before do + link['data-group'] = group.id.to_s + end + + it 'returns the nodes if the user can read the group' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_group, group). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array if the user can not read the group' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_group, group). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-group attribute' do + context 'with a data-project attribute' do + it 'returns the nodes if the attribute value equals the current project ID' do + link['data-project'] = project.id.to_s + + expect(Ability.abilities).not_to receive(:allowed?) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns the nodes if the user can read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array if the user can not read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'without a data-project attribute' do + it 'returns the nodes' do + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + end + end + end + + describe '#nodes_user_can_reference' do + context 'when the link has a data-author attribute' do + it 'returns the nodes when the user is a member of the project' do + other_project = create(:project) + other_project.team << [user, :developer] + + link['data-project'] = other_project.id.to_s + link['data-author'] = user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the project could not be found' do + link['data-project'] = '' + link['data-author'] = user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([]) + end + + it 'returns an empty Array when the user could not be found' do + other_project = create(:project) + + link['data-project'] = other_project.id.to_s + link['data-author'] = '' + + expect(subject.nodes_user_can_reference(user, [link])).to eq([]) + end + + it 'returns an empty Array when the user is not a team member' do + other_project = create(:project) + + link['data-project'] = other_project.id.to_s + link['data-author'] = user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-author attribute' do + it 'returns the nodes' do + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end + end + end +end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 3a2b568f4c7..898f1e84ab0 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -4,131 +4,185 @@ describe Ci::Ansi2html, lib: true do subject { Ci::Ansi2html } it "prints non-ansi as-is" do - expect(subject.convert("Hello")).to eq('Hello') + expect(subject.convert("Hello")[:html]).to eq('Hello') end it "strips non-color-changing controll sequences" do - expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world') + expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world') end it "prints simply red" do - expect(subject.convert("\e[31mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('Hello') end it "prints simply red without trailing reset" do - expect(subject.convert("\e[31mHello")).to eq('Hello') + expect(subject.convert("\e[31mHello")[:html]).to eq('Hello') end it "prints simply yellow" do - expect(subject.convert("\e[33mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('Hello') end it "prints default on blue" do - expect(subject.convert("\e[39;44mHello")).to eq('Hello') + expect(subject.convert("\e[39;44mHello")[:html]).to eq('Hello') end it "prints red on blue" do - expect(subject.convert("\e[31;44mHello")).to eq('Hello') + expect(subject.convert("\e[31;44mHello")[:html]).to eq('Hello') end it "resets colors after red on blue" do - expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('Hello world') end it "performs color change from red/blue to yellow/blue" do - expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('Hello world') end it "performs color change from red/blue to yellow/green" do - expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('Hello world') end it "performs color change from red/blue to reset to yellow/green" do - expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('Hello world') end it "ignores unsupported codes" do - expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello') end it "prints light red" do - expect(subject.convert("\e[91mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('Hello') end it "prints default on light red" do - expect(subject.convert("\e[101mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('Hello') end it "performs color change from red/blue to default/blue" do - expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('Hello world') + expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('Hello world') end it "performs color change from light red/blue to default/blue" do - expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('Hello world') + expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('Hello world') end it "prints bold text" do - expect(subject.convert("\e[1mHello")).to eq('Hello') + expect(subject.convert("\e[1mHello")[:html]).to eq('Hello') end it "resets bold text" do - expect(subject.convert("\e[1mHello\e[21m world")).to eq('Hello world') - expect(subject.convert("\e[1mHello\e[22m world")).to eq('Hello world') + expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('Hello world') + expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('Hello world') end it "prints italic text" do - expect(subject.convert("\e[3mHello")).to eq('Hello') + expect(subject.convert("\e[3mHello")[:html]).to eq('Hello') end it "resets italic text" do - expect(subject.convert("\e[3mHello\e[23m world")).to eq('Hello world') + expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('Hello world') end it "prints underlined text" do - expect(subject.convert("\e[4mHello")).to eq('Hello') + expect(subject.convert("\e[4mHello")[:html]).to eq('Hello') end it "resets underlined text" do - expect(subject.convert("\e[4mHello\e[24m world")).to eq('Hello world') + expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('Hello world') end it "prints concealed text" do - expect(subject.convert("\e[8mHello")).to eq('Hello') + expect(subject.convert("\e[8mHello")[:html]).to eq('Hello') end it "resets concealed text" do - expect(subject.convert("\e[8mHello\e[28m world")).to eq('Hello world') + expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('Hello world') end it "prints crossed-out text" do - expect(subject.convert("\e[9mHello")).to eq('Hello') + expect(subject.convert("\e[9mHello")[:html]).to eq('Hello') end it "resets crossed-out text" do - expect(subject.convert("\e[9mHello\e[29m world")).to eq('Hello world') + expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('Hello world') end it "can print 256 xterm fg colors" do - expect(subject.convert("\e[38;5;16mHello")).to eq('Hello') + expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('Hello') end it "can print 256 xterm fg colors on normal magenta background" do - expect(subject.convert("\e[38;5;16;45mHello")).to eq('Hello') + expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('Hello') end it "can print 256 xterm bg colors" do - expect(subject.convert("\e[48;5;240mHello")).to eq('Hello') + expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('Hello') end it "can print 256 xterm bg colors on normal magenta foreground" do - expect(subject.convert("\e[48;5;16;35mHello")).to eq('Hello') + expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('Hello') end it "prints bold colored text vividly" do - expect(subject.convert("\e[1;31mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('Hello') end it "prints bold light colored text correctly" do - expect(subject.convert("\e[1;91mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('Hello') + end + + it "prints <" do + expect(subject.convert("<")[:html]).to eq('<') + end + + describe "incremental update" do + shared_examples 'stateable converter' do + let(:pass1) { subject.convert(pre_text) } + let(:pass2) { subject.convert(pre_text + text, pass1[:state]) } + + it "to returns html to append" do + expect(pass2[:append]).to be_truthy + expect(pass2[:html]).to eq(html) + expect(pass1[:text] + pass2[:text]).to eq(pre_text + text) + expect(pass1[:html] + pass2[:html]).to eq(pre_html + html) + end + end + + context "with split word" do + let(:pre_text) { "\e[1mHello" } + let(:pre_html) { "Hello" } + let(:text) { "\e[1mWorld" } + let(:html) { "World" } + + it_behaves_like 'stateable converter' + end + + context "with split sequence" do + let(:pre_text) { "\e[1m" } + let(:pre_html) { "" } + let(:text) { "Hello" } + let(:html) { "Hello" } + + it_behaves_like 'stateable converter' + end + + context "with partial sequence" do + let(:pre_text) { "Hello\e" } + let(:pre_html) { "Hello" } + let(:text) { "[1m World" } + let(:html) { " World" } + + it_behaves_like 'stateable converter' + end + + context 'with new line' do + let(:pre_text) { "Hello\r" } + let(:pre_html) { "Hello\r" } + let(:text) { "\nWorld" } + let(:html) { "
    World" } + + it_behaves_like 'stateable converter' + end end end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 50a77308cde..9d1215a5760 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -12,5 +12,12 @@ describe Ci::Charts, lib: true do chart = Ci::Charts::BuildTime.new(@commit.project) expect(chart.build_times).to eq([2]) end + + it 'should handle nil build times' do + create(:ci_commit, duration: nil, project: @commit.project) + + chart = Ci::Charts::BuildTime.new(@commit.project) + expect(chart.build_times).to eq([2, 0]) + end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c7ab3185378..7375539cf17 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -443,12 +443,12 @@ module Ci context 'when job variables are defined' do context 'when syntax is correct' do it 'returns job variables' do - variables = { + variables = { KEY1: 'value1', SOME_KEY_2: 'value2' } - config = YAML.dump( + config = YAML.dump( { before_script: ['pwd'], rspec: { variables: variables, @@ -619,19 +619,19 @@ module Ci context 'no dependencies' do let(:dependencies) { } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context 'dependencies to builds' do let(:dependencies) { ['build1', 'build2'] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context 'dependencies to builds defined as symbols' do let(:dependencies) { [:build1, :build2] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context 'undefined dependency' do diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb new file mode 100644 index 00000000000..4d8cb787dde --- /dev/null +++ b/spec/lib/container_registry/blob_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe ContainerRegistry::Blob do + let(:digest) { 'sha256:0123456789012345' } + let(:config) do + { + 'digest' => digest, + 'mediaType' => 'binary', + 'size' => 1000 + } + end + + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + let(:blob) { repository.blob(config) } + + 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) } + + context '#path' do + subject { blob.path } + + it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') } + end + + context '#digest' do + subject { blob.digest } + + it { is_expected.to eq(digest) } + end + + context '#type' do + subject { blob.type } + + it { is_expected.to eq('binary') } + end + + context '#revision' do + subject { blob.revision } + + it { is_expected.to eq('0123456789012345') } + end + + context '#short_revision' do + subject { blob.short_revision } + + it { is_expected.to eq('012345678') } + end + + context '#delete' do + before do + stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). + to_return(status: 200) + end + + subject { blob.delete } + + it { is_expected.to be_truthy } + end +end diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb new file mode 100644 index 00000000000..4f3f8b24fc4 --- /dev/null +++ b/spec/lib/container_registry/registry_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ContainerRegistry::Registry do + let(:path) { nil } + let(:registry) { described_class.new('http://example.com', path: path) } + + subject { registry } + + it { is_expected.to respond_to(:client) } + it { is_expected.to respond_to(:uri) } + it { is_expected.to respond_to(:path) } + + it { expect(subject.repository('test')).not_to be_nil } + + context '#path' do + subject { registry.path } + + context 'path from URL' do + it { is_expected.to eq('example.com') } + end + + context 'custom path' do + let(:path) { 'registry.example.com' } + + it { is_expected.to eq(path) } + end + end +end diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb new file mode 100644 index 00000000000..279709521c9 --- /dev/null +++ b/spec/lib/container_registry/repository_spec.rb @@ -0,0 +1,65 @@ +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/vnd.docker.distribution.manifest.v2+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 new file mode 100644 index 00000000000..858cb0bb134 --- /dev/null +++ b/spec/lib/container_registry/tag_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe ContainerRegistry::Tag do + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + let(:tag) { repository.tag('tag') } + let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } + + 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) } + + context '#path' do + subject { tag.path } + + it { is_expected.to eq('example.com/group/test:tag') } + end + + context 'manifest processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/manifests/tag'). + with(headers: headers). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'), + headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) + end + + context '#layers' do + subject { tag.layers } + + it { expect(subject.length).to eq(1) } + end + + context '#total_size' do + subject { tag.total_size } + + it { is_expected.to eq(2319870) } + end + + context 'config processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + with(headers: { 'Accept' => 'application/octet-stream' }). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) + end + + context '#config' do + subject { tag.config } + + it { is_expected.not_to be_nil } + end + + context '#created_at' do + subject { tag.created_at } + + it { is_expected.not_to be_nil } + end + end + end + + context 'manifest digest' do + before do + stub_request(:head, 'http://example.com/v2/group/test/manifests/tag'). + with(headers: headers). + to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) + end + + context '#digest' do + subject { tag.digest } + + it { is_expected.to eq('sha256:digest') } + end + + context '#delete' do + before do + stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest'). + with(headers: headers). + to_return(status: 200) + end + + subject { tag.delete } + + it { is_expected.to be_truthy } + end + end +end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb index 53f5d6c5c80..88a71528867 100644 --- a/spec/lib/gitlab/akismet_helper_spec.rb +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -6,8 +6,8 @@ describe Gitlab::AkismetHelper, type: :helper do before do allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) - current_application_settings.akismet_enabled = true - current_application_settings.akismet_api_key = '12345' + allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345') end describe '#check_for_spam?' do diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb index af839f42421..7718689e6d4 100644 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -59,7 +59,7 @@ describe Gitlab::BitbucketImport::Client, lib: true do bitbucket_access_token_secret: "test" } }) project.import_url = "ssh://git@bitbucket.org/test/test.git" - expect { described_class.from_project(project) }.to_not raise_error + expect { described_class.from_project(project) }.not_to raise_error end end end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index acca0b08bab..711a3e1c7d4 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do 'path/dir_1/subdir/subfile' => { size: 10 }, 'path/second_dir' => {}, 'path/second_dir/dir_3/file_2' => { size: 10 }, - 'path/second_dir/dir_3/file_3'=> { size: 10 }, - 'another_directory/'=> {}, + 'path/second_dir/dir_3/file_3' => { size: 10 }, + 'another_directory/' => {}, 'another_file' => {}, '/file/with/absolute_path' => {} } end @@ -122,7 +122,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do describe 'empty path', path: '' do subject { |example| path(example) } - it { is_expected.to_not have_parent } + it { is_expected.not_to have_parent } describe '#children' do subject { |example| path(example).children } diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb new file mode 100644 index 00000000000..35ade7a2be0 --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +describe Gitlab::Database::MigrationHelpers, lib: true do + let(:model) do + ActiveRecord::Migration.new.extend( + Gitlab::Database::MigrationHelpers + ) + end + + before { allow(model).to receive(:puts) } + + describe '#add_concurrent_index' do + context 'outside a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) + end + + context 'using PostgreSQL' do + it 'creates the index concurrently' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + + expect(model).to receive(:add_index). + with(:users, :foo, algorithm: :concurrently) + + model.add_concurrent_index(:users, :foo) + end + end + + context 'using MySQL' do + it 'creates a regular index' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:add_index). + with(:users, :foo) + + model.add_concurrent_index(:users, :foo) + end + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect { model.add_concurrent_index(:users, :foo) }. + to raise_error(RuntimeError) + end + end + end + + describe '#update_column_in_batches' do + before do + create_list(:empty_project, 5) + end + + it 'updates all the rows in a table' do + model.update_column_in_batches(:projects, :import_error, 'foo') + + expect(Project.where(import_error: 'foo').count).to eq(5) + end + + it 'updates boolean values correctly' do + model.update_column_in_batches(:projects, :archived, true) + + expect(Project.where(archived: true).count).to eq(5) + end + end + + describe '#add_column_with_default' do + context 'outside of a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) + + expect(model).to receive(:transaction).twice.and_yield + + expect(model).to receive(:add_column). + with(:projects, :foo, :integer, default: nil) + + expect(model).to receive(:change_column_default). + with(:projects, :foo, 10) + end + + it 'adds the column while allowing NULL values' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10) + + expect(model).not_to receive(:change_column_null) + + model.add_column_with_default(:projects, :foo, :integer, + default: 10, + allow_null: true) + end + + it 'adds the column while not allowing NULL values' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10) + + expect(model).to receive(:change_column_null). + with(:projects, :foo, false) + + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end + + it 'removes the added column whenever updating the rows fails' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10). + and_raise(RuntimeError) + + expect(model).to receive(:remove_column). + with(:projects, :foo) + + expect do + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end.to raise_error(RuntimeError) + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end.to raise_error(RuntimeError) + end + end + end +end diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index b2d7a799810..c19f33e2224 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Email::Message::RepositoryPush do let!(:author) { create(:author, name: 'Author') } let(:message) do - described_class.new(Notify, project.id, 'recipient@example.com', opts) + described_class.new(Notify, project.id, opts) end context 'new commits have been pushed to repository' do @@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#diffs' do subject { message.diffs } - it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) } + it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) } end describe '#diffs_count' do diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 0a7ca3ec848..0af249d8690 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -33,8 +33,8 @@ describe Gitlab::Gfm::ReferenceRewriter do end it { is_expected.to include issue_first.to_reference(new_project) } - it { is_expected.to_not include issue_second.to_reference(new_project) } - it { is_expected.to_not include merge_request.to_reference(new_project) } + it { is_expected.not_to include issue_second.to_reference(new_project) } + it { is_expected.not_to include merge_request.to_reference(new_project) } end context 'description ambigous elements' do diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index eda956e6f0a..6eca33f9fee 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -32,13 +32,13 @@ describe Gitlab::Gfm::UploadsRewriter do let(:new_paths) { new_files.map(&:path) } it 'rewrites content' do - expect(new_text).to_not eq text + expect(new_text).not_to eq text expect(new_text.length).to eq text.length end it 'copies files' do expect(new_files).to all(exist) - expect(old_paths).to_not match_array new_paths + expect(old_paths).not_to match_array new_paths expect(old_paths).to all(include(old_project.path_with_namespace)) expect(new_paths).to all(include(new_project.path_with_namespace)) end @@ -48,8 +48,8 @@ describe Gitlab::Gfm::UploadsRewriter do end it 'generates a new secret for each file' do - expect(new_paths).to_not include image_uploader.secret - expect(new_paths).to_not include zip_uploader.secret + expect(new_paths).not_to include image_uploader.secret + expect(new_paths).not_to include zip_uploader.secret end end diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb new file mode 100644 index 00000000000..3cb634ba010 --- /dev/null +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::BranchFormatter, lib: true do + let(:project) { create(:project) } + let(:repo) { double } + let(:raw) do + { + ref: 'feature', + repo: repo, + sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + } + end + + describe '#exists?' do + it 'returns true when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.exists?).to eq true + end + + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.exists?).to eq false + end + end + + describe '#name' do + it 'returns raw ref when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.name).to eq 'feature' + end + + it 'returns formatted ref when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.name).to eq 'removed-branch-2e5d3239' + end + end + + describe '#repo' do + it 'returns raw repo' do + branch = described_class.new(project, double(raw)) + + expect(branch.repo).to eq repo + end + end + + describe '#sha' do + it 'returns raw sha' do + branch = described_class.new(project, double(raw)) + + expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end + + describe '#valid?' do + it 'returns true when repository exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.valid?).to eq true + end + + it 'returns false when repository does not exist' do + branch = described_class.new(project, double(raw.merge(repo: nil))) + + expect(branch.valid?).to eq false + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e59c0ca110e..120f59e6e71 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo) } + let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo) } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -41,8 +41,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'opened', milestone: nil, author_id: project.creator_id, @@ -66,8 +68,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'closed', milestone: nil, author_id: project.creator_id, @@ -91,8 +95,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'merged', milestone: nil, author_id: project.creator_id, @@ -137,11 +143,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:milestone) { double(number: 45) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } - it 'returns nil when milestone does not exists' do + it 'returns nil when milestone does not exist' do expect(pull_request.attributes.fetch(:milestone)).to be_nil end - it 'returns milestone when is exists' do + it 'returns milestone when it exists' do milestone = create(:milestone, project: project, iid: 45) expect(pull_request.attributes.fetch(:milestone)).to eq milestone @@ -158,36 +164,16 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#valid?' do - let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object } - - context 'when source, and target repositories are the same' do - context 'and source and target branches exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) } - - it 'returns true' do - expect(pull_request.valid?).to eq true - end - end - - context 'and source branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end - end - - context 'and target branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) } + context 'when source, and target repos are not a fork' do + let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false - end + it 'returns true' do + expect(pull_request.valid?).to eq true end end context 'when source repo is a fork' do - let(:source_repo) { double(id: 2, fork: true) } + let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do @@ -196,7 +182,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when target repo is a fork' do - let(:target_repo) { double(id: 2, fork: true) } + let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/gitignore_spec.rb new file mode 100644 index 00000000000..72baa516cc4 --- /dev/null +++ b/spec/lib/gitlab/gitignore_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Gitignore do + subject { Gitlab::Gitignore } + + describe '.all' do + it 'strips the gitignore suffix' do + expect(subject.all.first.name).not_to end_with('.gitignore') + end + + it 'combines the globals and rest' do + all = subject.all.map(&:name) + + expect(all).to include('Vim') + expect(all).to include('Ruby') + end + end + + describe '.find' do + it 'returns nil if the file does not exist' do + expect(subject.find('mepmep-yadida')).to be nil + end + + it 'returns the Gitignore object of a valid file' do + ruby = subject.find('Ruby') + + expect(ruby).to be_a Gitlab::Gitignore + expect(ruby.name).to eq('Ruby') + end + end + + describe '#content' do + it 'loads the full file' do + gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore')) + + expect(gitignore.name).to eq 'Ruby' + expect(gitignore.content).to start_with('*.gem') + end + end +end diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb deleted file mode 100644 index f758cb8693c..00000000000 --- a/spec/lib/gitlab/import_url_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ImportUrl do - - let(:credentials) { { user: 'blah', password: 'password' } } - let(:import_url) do - Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials) - end - - describe :full_url do - it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") } - end - - describe :sanitized_url do - it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") } - end - - describe :credentials do - it { expect(import_url.credentials).to eq(credentials) } - end -end diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb new file mode 100644 index 00000000000..b5ca89dd242 --- /dev/null +++ b/spec/lib/gitlab/lazy_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Lazy, lib: true do + let(:dummy) { double(:dummy) } + + context 'when not calling any methods' do + it 'does not call the supplied block' do + expect(dummy).not_to receive(:foo) + + described_class.new { dummy.foo } + end + end + + context 'when calling a method on the object' do + it 'lazy loads the value returned by the block' do + expect(dummy).to receive(:foo).and_return('foo') + + lazy = described_class.new { dummy.foo } + + expect(lazy.to_s).to eq('foo') + end + end + + describe '#respond_to?' do + it 'returns true for a method defined on the wrapped object' do + lazy = described_class.new { 'foo' } + + expect(lazy).to respond_to(:downcase) + end + + it 'returns false for a method not defined on the wrapped object' do + lazy = described_class.new { 'foo' } + + expect(lazy).not_to respond_to(:quack) + end + end +end diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index 5852b31ab3a..88814bc474d 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -26,8 +26,8 @@ describe Gitlab::Lfs::Router, lib: true do let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } - let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} - let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} describe 'when lfs is disabled' do before do @@ -368,7 +368,7 @@ describe Gitlab::Lfs::Router, lib: true do expect(response['objects']).to be_kind_of(Array) expect(response['objects'].first['oid']).to eq(sample_oid) expect(response['objects'].first['size']).to eq(sample_size) - expect(lfs_object.projects.pluck(:id)).to_not include(project.id) + expect(lfs_object.projects.pluck(:id)).not_to include(project.id) expect(lfs_object.projects.pluck(:id)).to include(public_project.id) expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}") expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) @@ -430,7 +430,7 @@ describe Gitlab::Lfs::Router, lib: true do expect(response_body['objects'].last['oid']).to eq(sample_oid) expect(response_body['objects'].last['size']).to eq(sample_size) - expect(response_body['objects'].last).to_not have_key('actions') + expect(response_body['objects'].last).not_to have_key('actions') end end end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 5c885a7a982..220e86924a2 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -56,9 +56,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -70,7 +67,7 @@ describe Gitlab::Metrics::Instrumentation do allow(Gitlab::Metrics).to receive(:method_call_threshold). and_return(100) - expect(transaction).to_not receive(:add_metric) + expect(transaction).not_to receive(:add_metric) @dummy.foo end @@ -139,9 +136,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') @@ -153,7 +147,7 @@ describe Gitlab::Metrics::Instrumentation do allow(Gitlab::Metrics).to receive(:method_call_threshold). and_return(100) - expect(transaction).to_not receive(:add_metric) + expect(transaction).not_to receive(:add_metric) @dummy.new.bar end @@ -226,7 +220,7 @@ describe Gitlab::Metrics::Instrumentation do described_class.instrument_methods(@dummy) - expect(@dummy).to_not respond_to(:_original_kittens) + expect(@dummy).not_to respond_to(:_original_kittens) end it 'can take a block to determine if a method should be instrumented' do @@ -234,7 +228,7 @@ describe Gitlab::Metrics::Instrumentation do false end - expect(@dummy).to_not respond_to(:_original_foo) + expect(@dummy).not_to respond_to(:_original_foo) end end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb index 38da77adc9f..59db127674a 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -130,7 +130,7 @@ describe Gitlab::Metrics::Sampler do 100.times do interval = sampler.sleep_interval - expect(interval).to_not eq(last) + expect(interval).not_to eq(last) last = interval end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index e3293a01207..49699ffe28f 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do describe 'without a current transaction' do it 'simply returns' do expect_any_instance_of(Gitlab::Metrics::Transaction). - to_not receive(:increment) + not_to receive(:increment) subscriber.sql(event) end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index e01b0b4bd21..d824dc54438 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_read' do it 'increments the cache_read duration' do expect(subscriber).to receive(:increment). - with(:cache_read_duration, event.duration) + with(:cache_read, event.duration) subscriber.cache_read(event) end @@ -18,7 +18,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_write' do it 'increments the cache_write duration' do expect(subscriber).to receive(:increment). - with(:cache_write_duration, event.duration) + with(:cache_write, event.duration) subscriber.cache_write(event) end @@ -27,7 +27,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_delete' do it 'increments the cache_delete duration' do expect(subscriber).to receive(:increment). - with(:cache_delete_duration, event.duration) + with(:cache_delete, event.duration) subscriber.cache_delete(event) end @@ -36,7 +36,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_exist?' do it 'increments the cache_exists duration' do expect(subscriber).to receive(:increment). - with(:cache_exists_duration, event.duration) + with(:cache_exists, event.duration) subscriber.cache_exist?(event) end @@ -61,10 +61,16 @@ describe Gitlab::Metrics::Subscribers::RailsCache do expect(transaction).to receive(:increment). with(:cache_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_count, 1) + expect(transaction).to receive(:increment). with(:cache_delete_duration, event.duration) - subscriber.increment(:cache_delete_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_delete_count, 1) + + subscriber.increment(:cache_delete, event.duration) end end end diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb new file mode 100644 index 00000000000..fd6f684db0c --- /dev/null +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::Middleware::RailsQueueDuration do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + let(:env) { {} } + let(:transaction) { double(:transaction) } + + before { expect(app).to receive(:call).with(env).and_return('yay') } + + describe '#call' do + it 'calls the app when metrics are disabled' do + expect(Gitlab::Metrics).to receive(:current_transaction).and_return(nil) + expect(middleware.call(env)).to eq('yay') + end + + context 'when metrics are enabled' do + before { allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) } + + it 'calls the app when metrics are enabled but no timing header is found' do + expect(middleware.call(env)).to eq('yay') + end + + it 'sets proxy_flight_time and calls the app when the header is present' do + env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123' + expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float)) + expect(middleware.call(env)).to eq('yay') + end + end + end +end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index f093d0a0d8b..e848d88182f 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -9,7 +9,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do before(:each) do expect(data).to have_key(:object_attributes) expect(data[:object_attributes]).to have_key(:url) - expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note)) + expect(data[:object_attributes][:url]) + .to eq(Gitlab::UrlBuilder.build(note)) expect(data[:object_kind]).to eq('note') expect(data[:user]).to eq(user.hook_attrs) end @@ -37,13 +38,21 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on issue' do - let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_issue, noteable_id: issue.id, project: project) } + let(:issue) do + create(:issue, created_at: fixed_time, updated_at: fixed_time, + project: project) + end + + let(:note) do + create(:note_on_issue, noteable: issue, project: project) + end it 'returns the note and issue-specific data' do expect(data).to have_key(:issue) - expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at')) - expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at'] + expect(data[:issue].except('updated_at')) + .to eq(issue.reload.hook_attrs.except('updated_at')) + expect(data[:issue]['updated_at']) + .to be > issue.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -51,13 +60,23 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on merge request' do - let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) } + let(:merge_request) do + create(:merge_request, created_at: fixed_time, + updated_at: fixed_time, + source_project: project) + end + + let(:note) do + create(:note_on_merge_request, noteable: merge_request, + project: project) + end it 'returns the note and merge request data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) - expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] + expect(data[:merge_request].except('updated_at')) + .to eq(merge_request.reload.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']) + .to be > merge_request.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -65,13 +84,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on merge request diff' do - let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) } + let(:merge_request) do + create(:merge_request, created_at: fixed_time, updated_at: fixed_time, + source_project: project) + end + + let(:note) do + create(:note_on_merge_request_diff, noteable: merge_request, + project: project) + end it 'returns the note and merge request diff data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) - expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] + expect(data[:merge_request].except('updated_at')) + .to eq(merge_request.reload.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']) + .to be > merge_request.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -79,13 +107,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on project snippet' do - let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } - let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) } + let!(:snippet) do + create(:project_snippet, created_at: fixed_time, updated_at: fixed_time, + project: project) + end + + let!(:note) do + create(:note_on_project_snippet, noteable: snippet, + project: project) + end it 'returns the note and project snippet data' do expect(data).to have_key(:snippet) - expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at')) - expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at'] + expect(data[:snippet].except('updated_at')) + .to eq(snippet.reload.hook_attrs.except('updated_at')) + expect(data[:snippet]['updated_at']) + .to be > snippet.hook_attrs['updated_at'] end include_examples 'project hook data' diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb index de6bb86c5dd..2ae79b50e77 100644 --- a/spec/lib/gitlab/sherlock/collection_spec.rb +++ b/spec/lib/gitlab/sherlock/collection_spec.rb @@ -11,13 +11,13 @@ describe Gitlab::Sherlock::Collection, lib: true do it 'adds a new transaction' do collection.add(transaction) - expect(collection).to_not be_empty + expect(collection).not_to be_empty end it 'is aliased as <<' do collection << transaction - expect(collection).to_not be_empty + expect(collection).not_to be_empty end end @@ -47,7 +47,7 @@ describe Gitlab::Sherlock::Collection, lib: true do it 'returns false for a collection with a transaction' do collection.add(transaction) - expect(collection).to_not be_empty + expect(collection).not_to be_empty end end diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb index 05da915ccfd..0a620428138 100644 --- a/spec/lib/gitlab/sherlock/query_spec.rb +++ b/spec/lib/gitlab/sherlock/query_spec.rb @@ -85,7 +85,7 @@ FROM users; frames = query.application_backtrace expect(frames).to be_an_instance_of(Array) - expect(frames).to_not be_empty + expect(frames).not_to be_empty frames.each do |frame| expect(frame.path).to start_with(Rails.root.to_s) diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb index 7553f2a045f..9fe18f253f0 100644 --- a/spec/lib/gitlab/sherlock/transaction_spec.rb +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -203,7 +203,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do end it 'only tracks queries triggered from the transaction thread' do - expect(transaction).to_not receive(:track_query) + expect(transaction).not_to receive(:track_query) Thread.new { subscription.publish('test', time, time, nil, query_data) }. join @@ -226,7 +226,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do end it 'only tracks views rendered from the transaction thread' do - expect(transaction).to_not receive(:track_view) + expect(transaction).not_to receive(:track_view) Thread.new { subscription.publish('test', time, time, nil, view_data) }. join diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb new file mode 100644 index 00000000000..de55334118f --- /dev/null +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Gitlab::UrlSanitizer, lib: true do + let(:credentials) { { user: 'blah', password: 'password' } } + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: credentials) + end + + describe '.sanitize' do + def sanitize_url(url) + # We want to try with multi-line content because is how error messages are formatted + described_class.sanitize(%Q{ + remote: Not Found + fatal: repository '#{url}' not found + }) + end + + it 'mask the credentials from HTTP URLs' do + filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/') + + expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/") + end + + it 'mask the credentials from HTTPS URLs' do + filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/') + + expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/") + end + + it 'mask credentials from SSH URLs' do + filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git') + + expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git") + end + + it 'does not modify Git URLs' do + # git protocol does not support authentication + filtered_content = sanitize_url('git://host.test/path/to/repo.git') + + expect(filtered_content).to include("git://host.test/path/to/repo.git") + end + + it 'does not modify scp-like URLs' do + filtered_content = sanitize_url('user@server:project.git') + + expect(filtered_content).to include("user@server:project.git") + end + end + + describe '#sanitized_url' do + it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } + end + + describe '#credentials' do + it { expect(url_sanitizer.credentials).to eq(credentials) } + end + + describe '#full_url' do + it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") } + + it 'supports scp-like URLs' do + sanitizer = described_class.new('user@server:project.git') + + expect(sanitizer.full_url).to eq('user@server:project.git') + end + end + +end diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb new file mode 100644 index 00000000000..18726754517 --- /dev/null +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -0,0 +1,43 @@ +describe JSONWebToken::RSAToken do + let(:rsa_key) do + OpenSSL::PKey::RSA.new <<-eos.strip_heredoc + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak + OUgAPF+rME4PB0Yl415DabUI40T5UNmlwxcCAwEAAQJAZtY2pSwIFm3JAXIh0cZZ + iXcAfiJ+YzuqinUOS+eW2sBCAEzjcARlU/o6sFQgtsOi4FOMczAd1Yx8UDMXMmrw + 2QIhAPBgVhJiTF09pdmeFWutCvTJDlFFAQNbrbo2X2x/9WF9AiEAzLgqMKeStSRu + H9N16TuDrUoO8R+DPqriCwkKrSHaWyMCIFzMhE4inuKcSywBaLmiG4m3GQzs++Al + A6PRG/PSTpQtAiBxtBg6zdf+JC3GH3zt/dA0/10tL4OF2wORfYQghRzyYQIhAL2l + 0ZQW+yLIZAGrdBFWYEAa52GZosncmzBNlsoTgwE4 + -----END RSA PRIVATE KEY----- + eos + end + let(:rsa_token) { described_class.new(nil) } + let(:rsa_encoded) { rsa_token.encoded } + + before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + + context 'token' do + context 'for valid key to be validated' do + before { rsa_token['key'] = 'value' } + + subject { JWT.decode(rsa_encoded, rsa_key) } + + it { expect{subject}.not_to raise_error } + it { expect(subject.first).to include('key' => 'value') } + it do + expect(subject.second).to eq( + "typ" => "JWT", + "alg" => "RS256", + "kid" => "OGXY:4TR7:FAVO:WEM2:XXEW:E4FP:TKL7:7ACK:TZAF:D54P:SUIA:P3B2") + end + end + + context 'for invalid key to raise an exception' do + let(:new_key) { OpenSSL::PKey::RSA.generate(512) } + subject { JWT.decode(rsa_encoded, new_key) } + + it { expect{subject}.to raise_error(JWT::DecodeError) } + end + end +end diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb new file mode 100644 index 00000000000..3d955e4d774 --- /dev/null +++ b/spec/lib/json_web_token/token_spec.rb @@ -0,0 +1,18 @@ +describe JSONWebToken::Token do + let(:token) { described_class.new } + + context 'custom parameters' do + let(:value) { 'value' } + before { token[:key] = value } + + it { expect(token[:key]).to eq(value) } + it { expect(token.payload).to include(key: value) } + end + + context 'embeds default payload' do + subject { token.payload } + let(:default) { token.send(:default_payload) } + + it { is_expected.to include(default) } + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 495c5cbac00..818825b1477 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -51,7 +51,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) end it 'contains a link to note author' do @@ -230,7 +230,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) end it 'contains a link to note author' do @@ -454,7 +454,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) end it 'contains a link to note author' do @@ -593,7 +593,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -606,10 +606,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Pushed new branch master/ end @@ -624,7 +620,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -637,10 +633,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Pushed new tag v1\.0/ end @@ -654,7 +646,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -667,10 +659,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Deleted branch master/ end @@ -680,7 +668,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -693,10 +681,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Deleted tag v1\.0/ end @@ -709,8 +693,9 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } + let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -723,10 +708,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ end @@ -735,15 +716,15 @@ describe Notify do is_expected.to have_body_text /Change some files/ end - it 'includes diffs' do - is_expected.to have_body_text /def archive_formats_regex/ + it 'includes diffs with character-level highlighting' do + is_expected.to have_body_text /def<\/span> archive_formats_regex/ end it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end - it 'doesn not contain the misleading footer' do + it 'does not contain the misleading footer' do is_expected.not_to have_body_text /you are a member of/ end @@ -817,8 +798,9 @@ describe Notify do let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } + let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like "a user cannot unsubscribe through footer link" @@ -831,10 +813,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /#{commits.first.title}/ end @@ -843,8 +821,8 @@ describe Notify do is_expected.to have_body_text /Change some files/ end - it 'includes diffs' do - is_expected.to have_body_text /def archive_formats_regex/ + it 'includes diffs with character-level highlighting' do + is_expected.to have_body_text /def<\/span> archive_formats_regex/ end it 'contains a link to the diff' do diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb new file mode 100644 index 00000000000..dc3062a4332 --- /dev/null +++ b/spec/mailers/previews/devise_mailer_preview.rb @@ -0,0 +1,11 @@ +class DeviseMailerPreview < ActionMailer::Preview + def confirmation_instructions_for_signup + user = User.new(name: 'Jane Doe', email: 'signup@example.com') + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + end + + def confirmation_instructions_for_new_email + user = User.last + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + end +end diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 5a85cb501dd..93de5850ba2 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -146,8 +146,8 @@ shared_examples 'it should have Gmail Actions links' do end shared_examples 'it should not have Gmail Actions links' do - it { is_expected.to_not have_body_text '