summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomasz Maczukin <tomasz@maczukin.pl>2016-01-14 21:58:17 +0100
committerTomasz Maczukin <tomasz@maczukin.pl>2016-01-14 21:58:17 +0100
commit768721dc5895467130b9a3ada9c740a050f4fdff (patch)
treed96448edb9490825e8384a1b42bbfafa98140a33
parent7ed5bc5c9b69e6caf45541dc8f820aa59a20c160 (diff)
parent2f2aada3b6b900fdff276aafc3f93e5a1c1465c6 (diff)
downloadgitlab-ce-768721dc5895467130b9a3ada9c740a050f4fdff.tar.gz
Merge branch 'master' into ci/api-triggers
* master: (150 commits) fixes margin and padding Update mailroom/postfix documentation [ci skip] Fix css mess around git clone panel. Align it properly Fix missing padding for user/group pages Fix parse_gollum_tags matcher Update documentation on Banzai::Filter::GollumTagsFilter Add tests for the wiki pipeline Refactoring Banzai::Filter::GollumTagsFilter Make sure the .git is at the end on Gitlab::GithubImport::WikiFormatter Remove GollumTagsPipeline Refactoring Gitlab::GithubImport::Importer Remove unnecessary brackets on WIKI_SLUG_ID route constraints Move js function to removing accents to vendor/assets/javascripts Update CHANGELOG Use the WikiPipeline when rendering the wiki markdown content Add Banzai::Filter::GollumTagsFilter for parsing Gollum's tags in HTML Relax constraints for wiki slug Import GitHub wiki into GitLab Move Ci::Build#available_statuses to AVAILABLE_STATUSES constant in CommitStatus Revert changes to how the notes are paginated in the API ... Conflicts: doc/api/README.md lib/api/entities.rb
-rw-r--r--CHANGELOG11
-rw-r--r--app/assets/javascripts/activities.js.coffee4
-rw-r--r--app/assets/javascripts/behaviors/autosize.js.coffee4
-rw-r--r--app/assets/javascripts/merge_request.js.coffee13
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee2
-rw-r--r--app/assets/javascripts/notes.js.coffee6
-rw-r--r--app/assets/javascripts/wikis.js.coffee25
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss25
-rw-r--r--app/assets/stylesheets/framework/buttons.scss74
-rw-r--r--app/assets/stylesheets/framework/common.scss69
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss7
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss25
-rw-r--r--app/assets/stylesheets/framework/mixins.scss35
-rw-r--r--app/assets/stylesheets/framework/mobile.scss9
-rw-r--r--app/assets/stylesheets/framework/nav.scss39
-rw-r--r--app/assets/stylesheets/framework/selects.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss3
-rw-r--r--app/assets/stylesheets/framework/tables.scss7
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss41
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/pages/commit.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss14
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss11
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss1
-rw-r--r--app/assets/stylesheets/pages/projects.scss226
-rw-r--r--app/assets/stylesheets/pages/tree.scss18
-rw-r--r--app/controllers/projects/artifacts_controller.rb56
-rw-r--r--app/controllers/projects/builds_controller.rb27
-rw-r--r--app/controllers/sent_notifications_controller.rb25
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/events_helper.rb8
-rw-r--r--app/helpers/gitlab_markdown_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb38
-rw-r--r--app/mailers/emails/merge_requests.rb55
-rw-r--r--app/mailers/emails/notes.rb44
-rw-r--r--app/mailers/notify.rb5
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/ci/build.rb41
-rw-r--r--app/models/commit_status.rb8
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/sent_notification.rb12
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/services/merge_requests/merge_service.rb2
-rw-r--r--app/views/admin/builds/_build.html.haml4
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml5
-rw-r--r--app/views/admin/users/_head.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml195
-rw-r--r--app/views/dashboard/_activities.html.haml4
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/milestones/show.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml48
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/show.html.haml30
-rw-r--r--app/views/help/ui.html.haml21
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/notify.html.haml8
-rw-r--r--app/views/profiles/accounts/show.html.haml2
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml13
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml7
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml11
-rw-r--r--app/views/projects/artifacts/browse.html.haml24
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml14
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml4
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml4
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml4
-rw-r--r--app/views/projects/commits/_head.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/graphs/_head.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/runners/index.html.haml3
-rw-r--r--app/views/projects/show.html.haml25
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml4
-rw-r--r--app/views/projects/wikis/_nav.html.haml2
-rw-r--r--app/views/projects/wikis/_new.html.haml11
-rw-r--r--app/views/projects/wikis/git_access.html.haml12
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_milestones_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml7
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml2
-rw-r--r--app/views/users/show.html.haml126
-rw-r--r--config/routes.rb15
-rw-r--r--db/fixtures/development/14_builds.rb79
-rw-r--r--db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb5
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/builds.md360
-rw-r--r--doc/incoming_email/README.md3
-rw-r--r--doc/incoming_email/postfix.md15
-rw-r--r--features/project/builds.feature58
-rw-r--r--features/project/wiki.feature5
-rw-r--r--features/steps/project/builds.rb89
-rw-r--r--features/steps/project/wiki.rb10
-rw-r--r--features/steps/shared/active_tab.rb4
-rw-r--r--features/steps/shared/builds.rb28
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/builds.rb149
-rw-r--r--lib/api/entities.rb27
-rw-r--r--lib/api/helpers.rb34
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb2
-rw-r--r--lib/banzai/filter/autolink_filter.rb1
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/emoji_filter.rb1
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb1
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb151
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/label_reference_filter.rb2
-rw-r--r--lib/banzai/filter/markdown_filter.rb1
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/redactor_filter.rb1
-rw-r--r--lib/banzai/filter/reference_filter.rb1
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb1
-rw-r--r--lib/banzai/filter/relative_link_filter.rb1
-rw-r--r--lib/banzai/filter/sanitization_filter.rb1
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb1
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb1
-rw-r--r--lib/banzai/filter/task_list_filter.rb1
-rw-r--r--lib/banzai/filter/upload_link_filter.rb1
-rw-r--r--lib/banzai/filter/user_reference_filter.rb2
-rw-r--r--lib/banzai/lazy_reference.rb2
-rw-r--r--lib/banzai/pipeline.rb2
-rw-r--r--lib/banzai/pipeline/asciidoc_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/atom_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/base_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/combined_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/email_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/full_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/note_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/plain_markdown_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/reference_extraction_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb11
-rw-r--r--lib/banzai/reference_extractor.rb2
-rw-r--r--lib/ci/api/builds.rb22
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb109
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb119
-rw-r--r--lib/gitlab/diff/parser.rb5
-rw-r--r--lib/gitlab/github_import/importer.rb27
-rw-r--r--lib/gitlab/github_import/project_creator.rb3
-rw-r--r--lib/gitlab/github_import/wiki_formatter.rb19
-rw-r--r--lib/gitlab/markdown/pipeline.rb2
-rw-r--r--lib/gitlab/reference_extractor.rb2
-rw-r--r--spec/controllers/sent_notification_controller_spec.rb26
-rw-r--r--spec/factories.rb7
-rw-r--r--spec/factories/ci/builds.rb11
-rw-r--r--spec/features/builds_spec.rb8
-rw-r--r--spec/features/markdown_spec.rb63
-rw-r--r--spec/fixtures/ci_build_artifacts.zipbin0 -> 106365 bytes
-rw-r--r--spec/fixtures/ci_build_artifacts_metadata.gzbin0 -> 415 bytes
-rw-r--r--spec/fixtures/markdown.md.erb9
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb5
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb89
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb84
-rw-r--r--spec/lib/gitlab/github_import/wiki_formatter_spec.rb22
-rw-r--r--spec/mailers/notify_spec.rb34
-rw-r--r--spec/models/build_spec.rb85
-rw-r--r--spec/models/project_wiki_spec.rb7
-rw-r--r--spec/requests/api/builds_spec.rb172
-rw-r--r--spec/requests/api/commit_status_spec.rb6
-rw-r--r--spec/requests/api/notes_spec.rb4
-rw-r--r--spec/requests/ci/api/builds_spec.rb46
-rw-r--r--spec/support/api/pagination_shared_examples.rb20
-rw-r--r--spec/support/markdown_feature.rb4
-rw-r--r--spec/support/matchers/markdown_matchers.rb18
-rwxr-xr-xvendor/assets/javascripts/autosize.js243
-rw-r--r--vendor/assets/javascripts/latinise.js11
217 files changed, 3225 insertions, 1063 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 2e0e26b4026..3dd3fcf3b87 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,11 @@
Please view this file on the master branch, on stable branches it's out of date.
+v 8.5.0 (unreleased)
+ - Remove gray background from layout in UI
+
v 8.4.0 (unreleased)
+ - Add pagination headers to already paginated API resources
+ - Properly generate diff of orphan commits, like the first commit in a repository
- Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages
- Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse)
- Improved performance of finding issues for an entire group (Yorick Peterse)
@@ -47,10 +52,15 @@ v 8.4.0 (unreleased)
- Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
- Add API support for managing project's build triggers
- Add API support for managing project's build variables
+ - Add API support for managing builds of a project
+ - Add API support for managing build variables of project
- Allow broadcast messages to be edited
+ - Autosize Markdown textareas
+ - Import GitHub wiki into GitLab
v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
+ - Add build artifacts browser
v 8.3.3
- Preserve CE behavior with JIRA integration by only calling API if URL is set
@@ -84,6 +94,7 @@ v 8.3.0
- Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
+ - Add unsubscribe link in the email footer (Zeger-Jan van de Weg)
- Provide better diagnostic message upon project creation errors (Stan Hu)
- Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
- Remove api credentials from link to build_page
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 63803747413..3b6b453ac51 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event-filter .btn").bind "click", (event) =>
+ $(".event-filter a").bind "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,7 +12,7 @@ class @Activities
toggleFilter: (sender) ->
- sender.toggleClass "active"
+ sender.closest('li').toggleClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
if event_filters
diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee
new file mode 100644
index 00000000000..b32072e61ee
--- /dev/null
+++ b/app/assets/javascripts/behaviors/autosize.js.coffee
@@ -0,0 +1,4 @@
+#= require autosize
+
+$ ->
+ autosize($('.js-autosize'))
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index ed0bf2b3f48..1f46e331427 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -48,14 +48,15 @@ class @MergeRequest
_this = @
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
$this = $(this)
- if $this.data('submitted')
- return
- e.preventDefault()
- e.stopImmediatePropagation()
shouldSubmit = $this.hasClass('btn-comment')
- console.log("shouldSubmit")
+ if shouldSubmit && $this.data('submitted')
+ return
if shouldSubmit
- _this.submitNoteForm($this.closest('form'),$this)
+ if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ _this.submitNoteForm($this.closest('form'),$this)
+
submitNoteForm: (form, $button) =>
noteText = form.find("textarea.js-note-text").val()
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 9e2dc1250c9..b10e1db7f3f 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -5,7 +5,7 @@
#
# ### Example Markup
#
-# <ul class="nav nav-tabs merge-request-tabs">
+# <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 8ba00ecbbab..356fb6aa08c 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -1,4 +1,5 @@
#= require autosave
+#= require autosize
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
@@ -246,6 +247,7 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
+ autosize(textarea)
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
@@ -521,9 +523,13 @@ class @Notes
if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen')
form.find('.js-note-target-close').text('Comment & close')
+ form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
+ form.find('.js-note-target-close').addClass('btn-comment-and-close')
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
+ form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
+ form.find('.js-note-target-close').removeClass('btn-comment-and-close')
initTaskList: ->
@enableTaskList()
diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee
index 81cfc37b956..19420f42468 100644
--- a/app/assets/javascripts/wikis.js.coffee
+++ b/app/assets/javascripts/wikis.js.coffee
@@ -1,17 +1,18 @@
+#= require latinise
+
class @Wikis
constructor: ->
- $('.build-new-wiki').bind "click", (e) ->
- $('[data-error~=slug]').addClass("hidden")
- $('p.hint').show()
+ $('.build-new-wiki').bind 'click', (e) =>
+ $('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
- valid_slug_pattern = /^[\w\/-]+$/
+ slug = @slugify(field.val())
- slug = field.val()
- if slug.match valid_slug_pattern
+ if (slug.length > 0)
path = field.attr('data-wikis-path')
- if(slug.length > 0)
- location.href = path + "/" + slug
- else
- e.preventDefault()
- $('p.hint').hide()
- $('[data-error~=slug]').removeClass("hidden")
+ location.href = path + '/' + slug
+
+ dasherize: (value) ->
+ value.replace(/[_\s]+/g, '-')
+
+ slugify: (value) =>
+ @dasherize(value.trim().toLowerCase().latinise())
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 48a4971c8fc..fa7641b1676 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,6 +24,7 @@
@import "framework/lists.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
+@import "framework/nav.scss";
@import "framework/pagination.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index fa0e70847f3..d0f5d33bf4d 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -18,9 +18,9 @@
line-height: 36px;
}
-.content-block,
.gray-content-block {
- margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
margin-bottom: 0px;
@@ -86,10 +86,7 @@
.cover-block {
text-align: center;
background: $background-color;
- margin: -$gl-padding;
- margin-bottom: 0;
- padding: 44px $gl-padding;
- border-bottom: 1px solid $border-color;
+ padding-top: 44px;
position: relative;
.avatar-holder {
@@ -136,3 +133,19 @@
.block-connector {
margin-top: -1px;
}
+
+.nav-block {
+ .controls {
+ float: right;
+ margin-top: 11px;
+ }
+}
+
+.content-block {
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-color;
+
+ &.oneline-block {
+ line-height: 42px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index bb29829b7a1..c99292c3f83 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,12 +1,8 @@
@mixin btn-default {
@include border-radius(3px);
- border-width: 1px;
- border-style: solid;
- font-size: 15px;
+ font-size: $gl-font-size;
font-weight: 500;
- line-height: 18px;
- padding: 11px $gl-padding;
- letter-spacing: .4px;
+ padding: $gl-vert-padding $gl-padding;
&:focus,
&:active {
@@ -17,8 +13,6 @@
@mixin btn-middle {
@include btn-default;
- @include border-radius(3px);
- padding: 11px 24px;
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
@@ -74,16 +68,15 @@
@include btn-default;
@include btn-white;
+ &.btn-small,
&.btn-sm {
- padding: 5px 10px;
- }
-
- &.btn-nr {
- padding: 7px 10px;
+ padding: 4px 10px;
+ font-size: 13px;
+ line-height: 18px;
}
&.btn-xs {
- padding: 1px 5px;
+ padding: 2px 5px;
}
&.btn-success,
@@ -159,33 +152,42 @@
}
}
-.btn-group-next {
+.btn-clipboard {
+ border: none;
+ padding: 0 5px;
+}
+
+.input-group-btn {
.btn {
- padding: 9px 0px;
- font-size: 15px;
- color: #7f8fa4;
- border-color: #e7e9ed;
- width: 140px;
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
+ @include btn-gray;
+ @include btn-middle;
+
+ &:hover {
+ outline: none;
+ }
+
+ &:focus {
+ outline: none;
}
- &.active {
- border-color: $gl-info;
- background: $gl-info;
- color: #fff;
+ &:active {
+ outline: none;
+ }
- .badge {
- color: $gl-info;
- background-color: white;
- }
+ &.btn-clipboard {
+ padding-left: 15px;
+ padding-right: 15px;
}
}
-}
-.btn-clipboard {
- border: none;
+ .active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ border: 1px solid #c6cacf !important;
+ background-color: #e4e7ed !important;
+ }
+
+ .btn-green {
+ @include btn-green
+ }
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 11730000f85..05645116268 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -374,75 +374,6 @@ table {
}
}
-.center-top-menu, .left-top-menu {
- @include nav-menu;
- text-align: center;
- margin-top: 5px;
- margin-bottom: $gl-padding;
- height: auto;
- margin-top: -$gl-padding;
-
- &.no-bottom {
- margin-bottom: 0;
- }
-
- &.no-top {
- margin-top: 0;
- }
-
- li a {
- display: inline-block;
- padding-top: $gl-padding;
- padding-bottom: 11px;
- margin-bottom: -1px;
- }
-
- &.bottom-border {
- border-bottom: 1px solid $border-color;
- height: 57px;
- }
-
- &.wide {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- }
-}
-
-.left-top-menu {
- text-align: left;
- border-bottom: 1px solid #EEE;
-}
-
-.center-middle-menu {
- @include nav-menu;
- padding: 0;
- text-align: center;
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
- height: 58px;
- border-bottom: 1px solid $border-color;
-
- li {
- &:after {
- content: "|";
- color: $border-gray-light;
- }
-
- &:last-child {
- &:after {
- content: none;
- }
- }
-
- > a {
- display: inline-block;
- text-transform: uppercase;
- font-size: 13px;
- }
- }
-}
-
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index cbfd4bc29b6..6ee104ee31a 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -3,11 +3,8 @@
*
*/
.file-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
border: none;
- border-top: 1px solid #E7E9EE;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
&.readme-holder {
border-bottom: 0;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 82eb50ad4be..1bfd0213995 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -8,10 +8,12 @@
.flash-notice {
@extend .alert;
@extend .alert-info;
+ margin: 0;
}
.flash-alert {
@extend .alert;
@extend .alert-danger;
+ margin: 0;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index d0cb91a5b64..4dab806d50e 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -74,8 +74,6 @@ label {
.form-control {
@include box-shadow(none);
- height: 42px;
- padding: 8px $gl-padding;
}
.form-control-inline {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4dbbb56104b..ba5e72c8c5a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -28,6 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
+ border-bottom: 1px solid #EEE;
.container-fluid {
width: 100% !important;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index a1a9990241d..e901c78d02f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -5,8 +5,6 @@ html {
}
body {
- background-color: #F3F3F3 !important;
-
&.navless {
background-color: white !important;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index bbdb1c038c5..c6bc6fb324d 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -109,10 +109,8 @@ ul.content-list {
padding: 0;
> li {
- padding: $gl-padding;
+ padding: $gl-padding 0;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
color: $gl-gray;
.avatar {
@@ -133,6 +131,7 @@ ul.content-list {
.panel > .content-list {
li {
margin: 0;
+ padding: $gl-padding;
}
}
@@ -148,7 +147,7 @@ ul.controls {
> li {
float: left;
margin-right: 10px;
-
+
&:last-child {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 4a00a197d9a..6732343802a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,13 +65,6 @@
position: relative;
}
-.md-header {
- ul {
- float: left;
- margin-bottom: 1px;
- }
-}
-
.referenced-users {
color: #4c4e54;
padding-top: 10px;
@@ -85,28 +78,12 @@
box-shadow: none;
}
-.new_note,
-.edit_note,
-.detail-page-description,
-.milestone-description,
-.wiki-content,
-.merge-request-form {
- .nav-tabs {
- margin-bottom: 0;
- border: none;
-
- li a,
- li.active a {
- border: 1px solid #DDD;
- }
- }
-}
-
.markdown-area {
@include border-radius(0);
background: #FFF;
border: 1px solid #ddd;
min-height: 140px;
+ max-height: 430px;
padding: 5px;
box-shadow: none;
width: 100%;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 41fd890f14f..1d5000fe388 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -118,38 +118,3 @@
font-size: 16px;
line-height: 24px;
}
-
-@mixin nav-menu {
- padding: 0;
- margin: 0;
- list-style: none;
- height: 56px;
-
- li {
- display: inline-block;
-
- a {
- padding: 14px;
- font-size: 15px;
- line-height: 28px;
- color: #959494;
- border-bottom: 2px solid transparent;
-
- &:hover, &:active, &:focus {
- text-decoration: none;
- outline: none;
- }
- }
-
- &.active a {
- color: #616060;
- border-bottom: 2px solid #4688f1;
- }
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c00709fb6bb..0997dfc287c 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -9,7 +9,7 @@
padding-right: 5px;
}
- .nav.nav-tabs > li > a {
+ .nav-links > li > a {
padding: 10px;
font-size: 12px;
margin-right: 3px;
@@ -81,7 +81,7 @@
display: none;
}
- .center-top-menu, .left-top-menu {
+ .nav-links, .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
@@ -100,11 +100,6 @@
}
@media (max-width: $screen-sm-max) {
- .page-with-sidebar .content-wrapper {
- padding: 0;
- padding-top: 1px;
- }
-
.issues-filters {
.milestone-filter, .labels-filter {
display: none;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
new file mode 100644
index 00000000000..c537d97fb24
--- /dev/null
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -0,0 +1,39 @@
+.nav-links {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ height: auto;
+ border-bottom: 1px solid $border-color;
+
+ li {
+ display: inline-block;
+
+ a {
+ display: inline-block;
+ padding: 14px;
+ padding-top: $gl-padding;
+ padding-bottom: 11px;
+ margin-bottom: -1px;
+ font-size: 15px;
+ line-height: 28px;
+ color: #959494;
+ border-bottom: 2px solid transparent;
+
+ &:hover, &:active, &:focus {
+ text-decoration: none;
+ outline: none;
+ }
+ }
+
+ &.active a {
+ color: #000000;
+ border-bottom: 2px solid #4688f1;
+ }
+
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index af145191bc8..3ee3443e349 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -3,8 +3,8 @@
.select2-choice {
background: #FFF;
border-color: #DDD;
- height: 42px;
- padding: 8px $gl-padding;
+ height: 36px;
+ padding: 6px $gl-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 83243dd2457..540d0b03163 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -21,11 +21,10 @@
.content-wrapper {
width: 100%;
- padding: 20px;
.container-fluid {
background: #FFF;
- padding: $gl-padding;
+ padding: 0 $gl-padding;
&.container-blank {
background: none;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 793ab3d9bb9..c4e9f467ce4 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -1,13 +1,11 @@
.table-holder {
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
+ margin: 0;
}
table {
&.table {
margin-bottom: $gl-padding;
-
+
.dropdown-menu a {
text-decoration: none;
}
@@ -32,6 +30,7 @@ table {
}
th {
+ background-color: $background-color;
font-weight: normal;
font-size: 15px;
border-bottom: 1px solid $border-color !important;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index ff41e26ed8a..47b843e5e3d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,10 +5,8 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding;
+ padding: $gl-padding 0;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 94f0ed761df..88072606bf5 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -99,47 +99,6 @@
}
}
-// Nav tabs
-.nav.nav-tabs {
- margin-bottom: 15px;
-
- li {
- > a {
- margin-right: 5px;
- line-height: 20px;
- border-color: #EEE;
- color: #888;
- border-bottom: 1px solid #ddd;
- .badge {
- background-color: #eee;
- color: #888;
- text-shadow: 0 1px 1px #fff;
- }
- i.fa {
- line-height: 14px;
- }
- }
- &.active {
- > a {
- border-color: #CCC;
- border-bottom: 1px solid #fff;
- color: #333;
- font-weight: bold;
- }
- }
- }
-}
-
-.nav-tabs > li > a,
-.nav-pills > li > a {
- color: #666;
-}
-
-.nav-pills > .active > a > span > .badge {
- background-color: #fff;
- color: $gl-primary;
-}
-
/**
* fix to keep tooltips position in top navigation bar
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 63868a34e2a..cd0621cdbf3 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -46,7 +46,7 @@ $font-size-base: $gl-font-size;
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$padding-base-vertical: 9px;
+$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
$component-active-color: #fff;
$component-active-bg: $brand-info;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 714369d9f15..ab4f71af039 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -177,7 +177,7 @@ body {
}
.page-title {
- margin-top: 0px;
+ margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d0ff3248ce1..85ecdddda79 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -22,6 +22,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
+$gl-vert-padding: 6px;
$gl-padding-top:10px;
$gl-avatar-size: 46px;
$secondary-text: #7f8fa4;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 002bd7e8ca5..c3f27333fad 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -4,7 +4,7 @@
position: absolute;
top: 0px;
right: 4px;
- line-height: 40px;
+ line-height: 56px;
}
a.js-zen-leave {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 05d2f306d0f..6ec88bdd804 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -39,6 +39,8 @@
}
.commit-box {
+ border-top: 1px solid $border-color;
+
.commit-title {
margin: 0;
font-size: 23px;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index deab805dbc2..529a43548c8 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,7 +1,5 @@
.detail-page-header {
- margin: -$gl-padding;
- padding: 7px $gl-padding;
- margin-bottom: 0px;
+ padding: 11px 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index afd6fb73675..1e2b8b51827 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,9 +1,7 @@
// Common
.diff-file {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- border: none;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
+ border-top: none;
.diff-header {
position: relative;
@@ -23,14 +21,6 @@
}
}
- .diff-controls {
- .btn {
- padding: 0px 10px;
- font-size: 13px;
- line-height: 28px;
- }
- }
-
.commit-short-id {
font-family: $monospace_font;
font-size: smaller;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 984b4b91216..8fa15b35748 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,9 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
+ padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d4b44004f4f..eae3590a189 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -27,10 +27,10 @@
.project-issuable-filter {
.controls {
float: right;
- margin-top: 7px;
+ margin-top: 11px;
}
- .center-top-menu {
+ .nav-links {
text-align: left;
}
}
@@ -95,7 +95,7 @@
.cross-project-reference {
color: $gl-link-color;
-
+
span {
white-space: nowrap;
width: 85%;
@@ -105,8 +105,13 @@
text-overflow: ellipsis;
}
+ cite {
+ font-style: normal;
+ }
+
button {
float: right;
+ padding: 3px 5px;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index efd33df2e99..75f2ae80a92 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,9 +3,9 @@
*
*/
.mr-state-widget {
- background: #F7F8FA;
+ background: $background-color;
color: $gl-gray;
- border: 1px solid #dce0e6;
+ border: 1px solid $border-color;
@include border-radius(2px);
form {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index d86259f93fb..2c9a42f9892 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -159,6 +159,7 @@
.edit_note {
.markdown-area {
min-height: 140px;
+ max-height: 430px;
}
.note-form-actions {
background: transparent;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f24b71963a8..13b0ed769fc 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -26,6 +26,8 @@
}
.project-home-panel {
+ padding-bottom: 40px;
+ border-bottom: 1px solid $border-color;
.cover-controls {
.project-settings-dropdown {
@@ -51,6 +53,8 @@
}
.notifications-btn {
+ margin-top: -28px;
+
.fa-bell {
margin-right: 6px;
}
@@ -75,17 +79,6 @@
}
}
- .git-clone-holder {
- max-width: 498px;
-
- .form-control {
- background: #FFF;
- font-size: 14px;
- height: 42px;
- margin-left: -1px;
- }
- }
-
.visibility-level-label {
@extend .btn;
@extend .btn-gray;
@@ -98,11 +91,6 @@
}
}
- .git-clone-holder {
- display: inline-table;
- position: relative;
- }
-
.project-repo-buttons {
margin-top: 12px;
margin-bottom: 0px;
@@ -112,10 +100,22 @@
margin-bottom: 12px;
}
+ .clone-row {
+ .split-repo-buttons,
+ .project-clone-holder {
+ display: inline-block;
+ }
+
+ .split-repo-buttons {
+ margin: 0 12px;
+ }
+ }
+
.btn {
@include btn-gray;
text-transform: none;
}
+
.count-with-arrow {
display: inline-block;
position: relative;
@@ -160,8 +160,8 @@
border-style: solid;
font-size: 13px;
font-weight: 600;
- line-height: 20px;
- padding: 11px 16px;
+ line-height: 13px;
+ padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 10px;
text-align: center;
@@ -189,117 +189,6 @@
}
}
-.git-clone-holder {
- .project-home-dropdown + & {
- margin-right: 45px;
- }
-
- .clone-options {
- display: table-cell;
- a.btn {
- width: 100%;
- }
- }
-
- .form-control {
- cursor: auto;
- @extend .monospace;
- background: #FAFAFA;
- width: 101%;
- }
-
- .input-group-addon {
- background: #f7f8fa;
-
- &.git-protocols {
- padding: 0;
- border: none;
-
- .input-group-btn:last-child > .btn {
- @include border-radius-right(0);
-
- border-left: 1px solid #c6cacf;
- margin-left: -2px !important;
- }
- }
- }
-}
-
-.projects-search-form {
-
- .input-group .form-control {
- height: 42px;
- }
-}
-
-.input-group-btn {
- .btn {
- @include btn-gray;
- @include btn-middle;
-
- &:hover {
- outline: none;
- }
-
- &:focus {
- outline: none;
- }
-
- &:active {
- outline: none;
- }
-
- &.btn-clipboard {
- padding-left: 15px;
- padding-right: 15px;
- }
- }
-
- .active {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- }
-
- .btn-green {
- @include btn-green
- }
-
-}
-
-.split-repo-buttons {
- display: inline-table;
- margin: 0 12px 0 12px;
-
- .btn{
- @include btn-gray;
- @include btn-default;
- }
-
- .dropdown-toggle {
- margin: -5px;
- }
-}
-
-#notification-form {
- margin-left: 5px;
-}
-
-.dropdown-new {
- margin-left: -5px;
-}
-
-.open > .dropdown-new.btn {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- text-transform: none;
- color: #313236 !important;
- font-size: 15px;
-}
-
.dropdown-menu {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px);
@@ -351,28 +240,6 @@
color: #555;
}
-ul.nav.nav-projects-tabs {
- @extend .nav-tabs;
-
- padding-left: 8px;
-
- li {
- a {
- padding: 6px 25px;
- margin-top: 2px;
- border-color: #DDD;
- background-color: #EEE;
- text-shadow: 0 1px 1px white;
- color: #555;
- }
- &.active {
- a {
- font-weight: bold;
- }
- }
- }
-}
-
.project_member_row form {
margin: 0px;
}
@@ -399,9 +266,9 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb {
padding: 0;
- line-height: 42px;
background: transparent;
border: none;
+ line-height: 42px;
margin: 0;
> li + li:before {
@@ -416,11 +283,8 @@ ul.nav.nav-projects-tabs {
.top-area {
border-bottom: 1px solid #EEE;
- margin: 0 -16px;
- padding: 0 $gl-padding;
- height: 42px;
- ul.left-top-menu {
+ ul.nav-links {
display: inline-block;
width: 50%;
margin-bottom: 0px;
@@ -431,12 +295,12 @@ ul.nav.nav-projects-tabs {
width: 50%;
display: inline-block;
float: right;
- padding-top: 7px;
+ padding-top: 11px;
text-align: right;
.btn-green {
- margin-top: -2px;
margin-left: 10px;
+ float: right;
}
}
@@ -482,11 +346,11 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px;
padding-bottom: 4px;
- ul.nav-pills {
+ ul.nav {
display:inline-block;
}
- .nav-pills li {
+ .nav li {
display:inline;
}
@@ -523,8 +387,7 @@ pre.light-well {
}
.projects-search-form {
- margin: -$gl-padding;
- padding: $gl-padding;
+ padding: $gl-padding 0;
padding-bottom: 0;
margin-bottom: 0px;
@@ -574,10 +437,8 @@ pre.light-well {
@include basic-list;
.project-row {
- padding: $gl-padding;
+ padding: $gl-padding 0;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
&.no-description {
.project {
@@ -631,8 +492,6 @@ pre.light-well {
}
.project-last-commit {
- margin: 0 7px;
-
.ci-status {
margin-right: 16px;
}
@@ -662,9 +521,7 @@ pre.light-well {
}
.project-show-readme .readme-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- padding: ($gl-padding + 7px);
+ padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
@@ -672,3 +529,32 @@ pre.light-well {
position: relative;
}
}
+
+.git-clone-holder {
+ width: 498px;
+
+ .btn-clipboard {
+ border: 1px solid $border-color;
+ padding: 6px $gl-padding;
+ }
+
+ .project-home-dropdown + & {
+ margin-right: 45px;
+ }
+
+ .clone-options {
+ display: table-cell;
+ a.btn {
+ width: 100%;
+ }
+ }
+
+ .form-control {
+ @extend .monospace;
+ background: #FFF;
+ font-size: 14px;
+ margin-left: -1px;
+ cursor: auto;
+ width: 101%;
+ }
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 97505edeabf..6a6dd7dfc85 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,4 +1,7 @@
.tree-holder {
+ > .nav-block {
+ margin: 11px 0;
+ }
.file-finder {
width: 50%;
@@ -13,7 +16,7 @@
tr {
> td, > th {
- line-height: 28px;
+ line-height: 26px;
}
&:hover {
@@ -86,12 +89,14 @@
.blob-commit-info {
list-style: none;
+ padding: $gl-padding;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-bottom: none;
margin: 0;
- padding: 0;
- margin-bottom: 5px;
.commit {
- padding: $gl-padding 0;
+ padding: 0;
.commit-row-title {
.commit-row-message {
@@ -115,3 +120,8 @@
font-weight: normal;
color: $md-link-color;
}
+
+.tree-controls {
+ float: right;
+ margin-top: 11px;
+}
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
new file mode 100644
index 00000000000..dff0732bdfe
--- /dev/null
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -0,0 +1,56 @@
+class Projects::ArtifactsController < Projects::ApplicationController
+ layout 'project'
+ before_action :authorize_read_build_artifacts!
+
+ def download
+ unless artifacts_file.file_storage?
+ return redirect_to artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ return not_found!
+ end
+
+ send_file artifacts_file.path, disposition: 'attachment'
+ end
+
+ def browse
+ return render_404 unless build.artifacts?
+
+ directory = params[:path] ? "#{params[:path]}/" : ''
+ @entry = build.artifacts_metadata_entry(directory)
+
+ return render_404 unless @entry.exists?
+ end
+
+ def file
+ entry = build.artifacts_metadata_entry(params[:path])
+
+ if entry.exists?
+ render json: { archive: build.artifacts_file.path,
+ entry: Base64.encode64(entry.path) }
+ else
+ render json: {}, status: 404
+ end
+ end
+
+ private
+
+ def build
+ @build ||= project.builds.unscoped.find_by!(id: params[:build_id])
+ end
+
+ def artifacts_file
+ @artifacts_file ||= build.artifacts_file
+ end
+
+ def authorize_read_build_artifacts!
+ unless can?(current_user, :read_build_artifacts, @project)
+ if current_user.nil?
+ return authenticate_user!
+ else
+ return render_404
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 39d3ba26ba2..0e965966ffa 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_manage_builds!, except: [:index, :show, :status]
- before_action :authorize_download_build_artifacts!, only: [:download]
layout "project"
@@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController
redirect_to build_path(build)
end
- def download
- unless artifacts_file.file_storage?
- return redirect_to artifacts_file.url
- end
-
- unless artifacts_file.exists?
- return not_found!
- end
-
- send_file artifacts_file.path, disposition: 'attachment'
- end
-
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
@@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
- def artifacts_file
- build.artifacts_file
- end
-
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end
@@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController
return page_404
end
end
-
- def authorize_download_build_artifacts!
- unless can?(current_user, :download_build_artifacts, @project)
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
- end
- end
- end
end
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
new file mode 100644
index 00000000000..7271c933b9b
--- /dev/null
+++ b/app/controllers/sent_notifications_controller.rb
@@ -0,0 +1,25 @@
+class SentNotificationsController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def unsubscribe
+ @sent_notification = SentNotification.for(params[:id])
+ return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+
+ noteable = @sent_notification.noteable
+ noteable.unsubscribe(@sent_notification.recipient)
+
+ flash[:notice] = "You have been unsubscribed from this thread."
+ if current_user
+ case noteable
+ when Issue
+ redirect_to issue_path(noteable)
+ when MergeRequest
+ redirect_to merge_request_path(noteable)
+ else
+ redirect_to root_path
+ end
+ else
+ redirect_to new_user_session_path
+ end
+ end
+end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index ec0e3f409c1..d6c05843743 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,7 +17,7 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: 'btn btn-xs btn-clipboard',
+ class: 'btn btn-clipboard',
data: data,
type: :button
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index dde83ff36b5..31bf45baeb7 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -27,13 +27,15 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link btn btn-default #{active}",
+ class: "event-filter-link",
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
}
- link_to request.path, link_opts do
- content_tag(:span, ' ' + tooltip)
+ content_tag :li, class: active do
+ link_to request.path, link_opts do
+ content_tag(:span, ' ' + tooltip)
+ end
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index ca41657cec1..1a226252251 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -91,7 +91,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
- markdown(wiki_page.content)
+ markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
when :asciidoc
asciidoc(wiki_page.content)
else
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index abdeefed5ef..4a88cb61132 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,31 +1,31 @@
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @issue_status = status
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @issue_status = status
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
private
@@ -38,14 +38,12 @@ module Emails
}
end
- def issue_mail_with_notification(issue_id, recipient_id)
+ def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- yield
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 7923fb770d0..325996e2e16 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -1,77 +1,64 @@
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
mail_new_thread(@merge_request,
from: sender(@merge_request.author_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@updated_by = User.find updated_by_user_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@mr_status = status
- @project = @merge_request.project
@updated_by = User.find updated_by_user_id
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ end
+
+ private
+
+ def setup_merge_request_mail(merge_request_id, recipient_id)
+ @merge_request = MergeRequest.find(merge_request_id)
+ @project = @merge_request.project
+ @target_url = namespace_project_merge_request_url(@project.namespace,
+ @project,
+ @merge_request)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index e1382d2da12..f9650df9a74 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,31 +1,31 @@
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @commit = @note.noteable
- @target_url = namespace_project_commit_url(*note_target_url_options)
-
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @commit = @note.noteable
+ @target_url = namespace_project_commit_url(*note_target_url_options)
+
+ mail_answer_thread(@commit,
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
def note_issue_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @issue = @note.noteable
- @target_url = namespace_project_issue_url(*note_target_url_options)
- mail_answer_thread(@issue, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @issue = @note.noteable
+ @target_url = namespace_project_issue_url(*note_target_url_options)
+ mail_answer_thread(@issue, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @merge_request = @note.noteable
- @target_url = namespace_project_merge_request_url(*note_target_url_options)
- mail_answer_thread(@merge_request, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @merge_request = @note.noteable
+ @target_url = namespace_project_merge_request_url(*note_target_url_options)
+ mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
private
@@ -42,13 +42,11 @@ module Emails
}
end
- def note_mail_with_notification(note_id, recipient_id)
+ def setup_note_mail(note_id, recipient_id)
@note = Note.find(note_id)
@project = @note.project
- yield
-
- SentNotification.record_note(@note, recipient_id, reply_key)
+ @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 3bbdd9cee76..e1cd075a978 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -107,10 +107,9 @@ class Notify < BaseMailer
end
headers["X-GitLab-#{model.class.name}-ID"] = model.id
+ headers['X-GitLab-Reply-Key'] = reply_key
- if reply_key
- headers['X-GitLab-Reply-Key'] = reply_key
-
+ if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 5a1a67db8e1..5375148a654 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -175,7 +175,7 @@ class Ability
:create_merge_request,
:create_wiki,
:manage_builds,
- :download_build_artifacts,
+ :read_build_artifacts,
:push_code
]
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index a4779d06de8..6cc26abce66 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -30,10 +30,12 @@
# description :string(255)
# artifacts_file :text
# gl_project_id :integer
+# artifacts_metadata :text
#
module Ci
class Build < CommitStatus
+ include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
@@ -49,6 +51,7 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
+ mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
@@ -291,21 +294,18 @@ module Ci
end
def target_url
- Gitlab::Application.routes.url_helpers.
- namespace_project_build_url(project.namespace, project, self)
+ namespace_project_build_url(project.namespace, project, self)
end
def cancel_url
if active?
- Gitlab::Application.routes.url_helpers.
- cancel_namespace_project_build_path(project.namespace, project, self)
+ cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
if retryable?
- Gitlab::Application.routes.url_helpers.
- retry_namespace_project_build_path(project.namespace, project, self)
+ retry_namespace_project_build_path(project.namespace, project, self)
end
end
@@ -321,20 +321,35 @@ module Ci
pending? && !any_runners_online?
end
- def download_url
- if artifacts_file.exists?
- Gitlab::Application.routes.url_helpers.
- download_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def execute_hooks
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
end
+ def artifacts?
+ artifacts_file.exists?
+ end
+
+ def artifacts_download_url
+ if artifacts?
+ download_namespace_project_build_artifacts_path(project.namespace, project, self)
+ end
+ end
+ def artifacts_browse_url
+ if artifacts_browser_supported?
+ browse_namespace_project_build_artifacts_path(project.namespace, project, self)
+ end
+ end
+
+ def artifacts_browser_supported?
+ artifacts? && artifacts_metadata.exists?
+ end
+
+ def artifacts_metadata_entry(path)
+ Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry
+ end
private
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ff479493474..66e0502fc0c 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -56,6 +56,8 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
+ AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
+
state_machine :status, initial: :pending do
event :run do
transition pending: :running
@@ -131,7 +133,11 @@ class CommitStatus < ActiveRecord::Base
false
end
- def download_url
+ def artifacts_download_url
+ nil
+ end
+
+ def artifacts_browse_url
nil
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 18a00f95b48..04650a9e67a 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -119,6 +119,12 @@ module Issuable
update(subscribed: !subscribed?(user))
end
+ def unsubscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: false)
+ end
+
def to_hook_data(user)
{
object_kind: self.class.name.underscore,
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index b5fec38378b..8ce47495971 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -38,6 +38,10 @@ class ProjectWiki
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
+ def wiki_base_path
+ ["/", @project.path_with_namespace, "/wikis"].join('')
+ end
+
# Returns the Gollum::Wiki object.
def wiki
@wiki ||= begin
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f36eda1531b..77115597d71 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base
class << self
def reply_key
- return nil unless Gitlab::IncomingEmail.enabled?
-
SecureRandom.hex(16)
end
@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base
def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code
-
+
record(note.noteable, recipient_id, reply_key, params)
end
end
+ def unsubscribable?
+ !for_commit?
+ end
+
def for_commit?
noteable_type == "Commit"
end
@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base
super
end
end
+
+ def to_param
+ self.reply_key
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e9413c34bae..2a65f0431c4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -169,7 +169,7 @@ class WikiPage
private
def set_attributes
- attributes[:slug] = @page.escaped_url_path
+ attributes[:slug] = @page.url_path
attributes[:title] = @page.title
attributes[:format] = @page.format
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cabc3d8fabb..e8bef250d8b 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -44,7 +44,7 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch]
+ if params[:should_remove_source_branch].present?
DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch)
end
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 6936e614346..c395bd908c3 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -60,8 +60,8 @@
%td
.pull-right
- - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
- = link_to build.download_url, title: 'Download artifacts' do
+ - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts?
+ = link_to build.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, build.project)
- if build.active?
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index ddd4e1481eb..ebf2b7b60e7 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -4,7 +4,7 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- %ul.center-top-menu
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
All
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 1484baa78e0..af9fdeb0734 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,12 +1,13 @@
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
-%ul.nav.nav-tabs.log-tabs
+%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
-%p.light To prevent performance issues admin logs output the last 2000 lines
+.gray-content-block
+ To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index bc44b1b1d8e..ce5e21e54cc 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -12,7 +12,7 @@
%i.fa.fa-pencil-square-o
Edit
%hr
-%ul.nav.nav-tabs
+%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
+.append-bottom-default
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 8312642b6c3..b050a4d01c3 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,106 +1,101 @@
- page_title "Users"
= render 'shared/show_aside'
-.row
- %aside.col-md-3
- .admin-filter
- %ul.nav.nav-pills.nav-stacked
- %li{class: "#{'active' unless params[:filter]}"}
- = link_to admin_users_path do
- Active
- %small.pull-right= number_with_delimiter(User.active.count)
- %li{class: "#{'active' if params[:filter] == "admins"}"}
- = link_to admin_users_path(filter: "admins") do
- Admins
- %small.pull-right= number_with_delimiter(User.admins.count)
- %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- 2FA Enabled
- %small.pull-right= number_with_delimiter(User.with_two_factor.count)
- %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- 2FA Disabled
- %small.pull-right= number_with_delimiter(User.without_two_factor.count)
- %li{class: "#{'active' if params[:filter] == "blocked"}"}
- = link_to admin_users_path(filter: "blocked") do
- Blocked
- %small.pull-right= number_with_delimiter(User.blocked.count)
- %li{class: "#{'active' if params[:filter] == "wop"}"}
- = link_to admin_users_path(filter: "wop") do
- Without projects
- %small.pull-right= number_with_delimiter(User.without_projects.count)
- %hr
- = form_tag admin_users_path, method: :get, class: 'form-inline' do
- .form-group
- = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
- = hidden_field_tag "filter", params[:filter]
- = button_tag class: 'btn btn-primary' do
- %i.fa.fa-search
- %hr
- = link_to 'Reset', admin_users_path, class: "btn btn-cancel"
+.admin-filter
+ %ul.nav-links
+ %li{class: "#{'active' unless params[:filter]}"}
+ = link_to admin_users_path do
+ Active
+ %small.badge= number_with_delimiter(User.active.count)
+ %li{class: "#{'active' if params[:filter] == "admins"}"}
+ = link_to admin_users_path(filter: "admins") do
+ Admins
+ %small.badge= number_with_delimiter(User.admins.count)
+ %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ 2FA Enabled
+ %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ 2FA Disabled
+ %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li{class: "#{'active' if params[:filter] == "blocked"}"}
+ = link_to admin_users_path(filter: "blocked") do
+ Blocked
+ %small.badge= number_with_delimiter(User.blocked.count)
+ %li{class: "#{'active' if params[:filter] == "wop"}"}
+ = link_to admin_users_path(filter: "wop") do
+ Without projects
+ %small.badge= number_with_delimiter(User.without_projects.count)
- %section.col-md-9
- .panel.panel-default
- .panel-heading
- Users (#{number_with_delimiter(@users.total_count)})
- .panel-head-actions
- .dropdown.inline
- %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_name
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
- = sort_title_name
- = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
- = sort_title_recently_signin
- = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
- = sort_title_oldest_signin
- = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
- = sort_title_recently_created
- = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
- = sort_title_oldest_created
- = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
- = sort_title_recently_updated
- = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
- = sort_title_oldest_updated
-
- = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
- %ul.well-list
- - @users.each do |user|
+ .gray-content-block.second-block
+ .pull-right
+ .dropdown.inline
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %span.light sort:
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_name
+ %b.caret
+ %ul.dropdown-menu
%li
- .list-item-name
- - if user.blocked?
- %i.fa.fa-lock.cred
+ = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
+ = sort_title_name
+ = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
+ = sort_title_recently_signin
+ = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
+ = sort_title_oldest_signin
+ = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
+ = sort_title_recently_created
+ = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
+ = sort_title_oldest_created
+ = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
+ = sort_title_recently_updated
+ = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
+ = sort_title_oldest_updated
+
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ = form_tag admin_users_path, method: :get, class: 'form-inline' do
+ .form-group
+ = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
+ = hidden_field_tag "filter", params[:filter]
+ = button_tag class: 'btn btn-primary' do
+ %i.fa.fa-search
+
+
+.panel.panel-default
+ %ul.well-list
+ - @users.each do |user|
+ %li
+ .list-item-name
+ - if user.blocked?
+ %i.fa.fa-lock.cred
+ - else
+ %i.fa.fa-user.cgreen
+ = link_to user.name, [:admin, user]
+ - if user.admin?
+ %strong.cred (Admin)
+ - if user == current_user
+ %span.cred It's you!
+ .pull-right
+ %span.light
+ %i.fa.fa-envelope
+ = mail_to user.email, user.email, class: 'light'
+ &nbsp;
+ .pull-right
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
+ - unless user == current_user
+ - if user.ldap_blocked?
+ = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
+ %i.fa.fa-lock
+ Unblock
+ - elsif user.blocked?
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
- %i.fa.fa-user.cgreen
- = link_to user.name, [:admin, user]
- - if user.admin?
- %strong.cred (Admin)
- - if user == current_user
- %span.cred It's you!
- .pull-right
- %span.light
- %i.fa.fa-envelope
- = mail_to user.email, user.email, class: 'light'
- &nbsp;
- .pull-right
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- - unless user == current_user
- - if user.ldap_blocked?
- = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
- %i.fa.fa-lock
- Unblock
- - elsif user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- - else
- = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
- - if user.access_locked?
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
- = paginate @users, theme: "gitlab"
+ = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
+ - if user.access_locked?
+ = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ - if user.can_be_removed?
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
+= paginate @users, theme: "gitlab"
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index f98fd9f06ba..dc76599b776 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,9 +1,9 @@
.hidden-xs
= render "events/event_last_push", event: @last_push
-.gray-content-block
+.nav-block
- if current_user
- .pull-right
+ .controls
= link_to dashboard_projects_path(: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/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 9f4be025bf2..b78e70ebc1e 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
%li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 64bd356f546..6ca97a692b4 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
Your Groups
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f4a3e3162bf..5c4b58cd688 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,7 +1,7 @@
= content_for :flash_message do
= render 'shared/project_limit'
.top-area
- %ul.left-top-menu
+ %ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index 0ae62d6f1b6..b25e8ea1f0c 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 49a558e8ac9..3810267577c 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -48,7 +48,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
-%ul.center-top-menu.no-top.no-bottom
+%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 07b6d57932e..d4e7862981c 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -3,32 +3,36 @@
= render 'dashboard/snippets_head'
-.gray-content-block
- .pull-right
+.nav-block
+ .controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus')
New Snippet
- .btn-group.btn-group-next.snippet-scope-menu
- = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
- All
- %span.badge
- = current_user.snippets.count
-
- = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
- Public
- %span.badge
- = current_user.snippets.are_public.count
+ .nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to dashboard_snippets_path do
+ All
+ %span.badge
+ = current_user.snippets.count
+
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to dashboard_snippets_path(scope: 'are_private') do
+ Private
+ %span.badge
+ = current_user.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to dashboard_snippets_path(scope: 'are_internal') do
+ Internal
+ %span.badge
+ = current_user.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to dashboard_snippets_path(scope: 'are_public') do
+ Public
+ %span.badge
+ = current_user.snippets.are_public.count
= render 'snippets/snippets'
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 41ad2c231d4..2c15e2c4891 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,7 +7,7 @@
%h3 Sign in
.login-body
- if form_based_providers.any?
- %ul.nav.nav-tabs
+ %ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 7e3e2e28bc9..e2f97fd9337 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,7 +1,7 @@
- header_title group_title(@group, "Settings", edit_group_path(@group))
- @blank_container = true
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
Group settings
.panel-body
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 335bf036074..6a8acc42af9 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -2,7 +2,7 @@
- header_title group_title(@group, "Members", group_group_members_path(@group))
- @blank_container = true
-.group-members-page
+.group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default
.panel-heading
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index d063b257b5e..1233da85524 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -54,7 +54,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
-%ul.center-top-menu.no-top.no-bottom
+%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index f1d507a50c7..9ca11ed1177 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,7 +1,7 @@
- page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group))
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
%strong= @group.name
projects:
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 48a544fc834..ebb3df7dca3 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
- unless can?(current_user, :read_group, @group)
- @disable_search_panel = true
@@ -25,8 +27,8 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-- if can?(current_user, :read_group, @group)
- %ul.center-top-menu.no-top
+
+ %ul.nav-links
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
@@ -35,20 +37,22 @@
= link_to "#projects", 'data-toggle' => 'tab' do
Projects
- .tab-content
- .tab-pane.active#activity
- .gray-content-block.activity-filter-block
- - if current_user
- = render "events/event_last_push", event: @last_push
+- if can?(current_user, :read_group, @group)
+ %div{ class: container_class }
+ .tab-content
+ .tab-pane.active#activity
+ .activity-filter-block
+ - if current_user
+ = render "events/event_last_push", event: @last_push
- = render 'shared/event_filter'
+ = render 'shared/event_filter'
- .content_list
- = spinner
+ .content_list
+ = spinner
- .tab-pane#projects
- = render "projects", projects: @projects
+ .tab-pane#projects
+ = render "projects", projects: @projects
- else
- %p.center-top-menu.no-top
+ %p.nav-links.no-top
No projects to show
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d9ffda884c8..7b45bd09050 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -139,26 +139,9 @@
%h2#navs Navigation
%h4
- %code .center-top-menu
+ %code .nav-links
.example
- %ul.center-top-menu
- %li.active
- %a Open
- %li
- %a Closed
-
- %h4
- %code .btn-group.btn-group-next
- .example
- %div.btn-group.btn-group-next
- %a.btn.active Open
- %a.btn Closed
-
-
- %h4
- %code .nav.nav-tabs
- .example
- %ul.nav.nav-tabs
+ %ul.nav-links
%li.active
%a Open
%li
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec7cd79bc54..26159989777 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -24,7 +24,7 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
- %div{ class: container_class }
+ %div{ class: (container_class unless @no_container) }
.content
.clearfix
= yield
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 3ca4c340406..325c68c69dc 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -44,6 +44,10 @@
%br
-# Don't link the host is the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
- If you'd like to receive fewer emails, you can adjust your notification settings.
+ If you'd like to receive fewer emails, you can
+ - if @sent_notification && @sent_notification.unsubscribable?
+ = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ from this thread or
+ adjust your notification settings.
- = email_action @target_url \ No newline at end of file
+ = email_action @target_url
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 17e47c622ce..a42fd38de3a 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -6,7 +6,7 @@
.alert.alert-info
Some options are unavailable for LDAP accounts
-.account-page
+.account-page.prepend-top-default
.panel.panel-default.update-token
.panel-heading
Reset Private token
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 101880bd105..961b61d2e76 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,6 +1,6 @@
-.gray-content-block.activity-filter-block
+.nav-block.activity-filter-block
- if current_user
- .pull-right
+ .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index fa978325ddd..96c2fa87f45 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,5 +1,5 @@
#tree-holder.tree-holder.clearfix
- .gray-content-block.second-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 53eec76129b..298c6664997 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -44,13 +44,16 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- = render "shared/clone_panel"
+ .clone-row
+ .project-clone-holder
+ = render "shared/clone_panel"
- .split-repo-buttons
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
+ .split-repo-buttons
+ .btn-group.pull-left
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
- = render 'projects/buttons/notifications'
+ = render 'projects/buttons/notifications'
:javascript
new Star();
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 54c818baaf4..1fb37ef6621 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,6 +1,6 @@
.md-area
.md-header.clearfix
- %ul.center-top-menu
+ %ul.nav-links
%li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index d5829568275..e701253d7de 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,6 +1,6 @@
.zennable
.zen-backdrop
- - classes << ' js-gfm-input markdown-area'
+ - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes
- else
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
new file mode 100644
index 00000000000..5b87d55efd5
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -0,0 +1,7 @@
+%tr{ class: 'tree-item' }
+ %td.tree-item-file-name
+ = tree_icon('folder', '755', directory.name)
+ %span.str-truncated
+ = link_to directory.name, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
+ %td
+ %td
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
new file mode 100644
index 00000000000..92c1648f726
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -0,0 +1,11 @@
+%tr{ class: 'tree-item' }
+ %td.tree-item-file-name
+ = tree_icon('file', '664', file.name)
+ %span.str-truncated
+ = file.name
+ %td
+ = number_to_human_size(file.metadata[:size], precision: 2)
+ %td
+ = link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path),
+ class: 'btn btn-xs btn-default artifact-download' do
+ = icon('download')
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
new file mode 100644
index 00000000000..1add7ef6bfb
--- /dev/null
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -0,0 +1,24 @@
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+= render 'projects/builds/header_title'
+
+#tree-holder.tree-holder
+ .gray-content-block.top-block.clearfix
+ .pull-right
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
+ class: 'btn btn-default' do
+ = icon('download')
+ Download artifacts archive
+
+%div.tree-content-holder
+ .table-holder
+ %table.table.tree-table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Size
+ %th Download
+ = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory
+ = render partial: 'tree_file', collection: @entry.files, as: :file
+
+- if @entry.empty?
+ .center Empty
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 2a3315da3db..3d8d88834e2 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.top-block
+.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 09fa148b129..a279e6eda55 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
= render "header_title"
.file-editor
- %ul.center-top-menu.no-bottom.js-edit-mode
+ %ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 3bbfdb1e3b0..5d18c0d803a 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -8,7 +8,7 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- %ul.center-top-menu
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
All
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 5b7ecce86ab..2be572d3b10 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -14,7 +14,7 @@
#up-build-trace
- if @commit.matrix_for_ref?(@build.ref)
- %ul.center-top-menu.no-top.no-bottom
+ %ul.nav-links.no-top.no-bottom
- @commit.latest_builds_for_ref(@build.ref).each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
@@ -89,9 +89,15 @@
Test coverage
%h1 #{@build.coverage}%
- - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
- .build-widget.center
- = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
+ - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts?
+
+ .build-widget.artifacts
+ %h4.title Build artifacts
+ .center
+ .btn-group{ role: :group }
+ = link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary'
+ - if @build.artifacts_browser_supported?
+ = link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary'
.build-widget
%h4.title
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index f9ab78e7874..511863d774e 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
- %span.dropdown
- %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ .btn-group
+ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f74f8b427ec..ea33aa472a6 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
+%ul.nav-links.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index ddb77fd796b..bbe820b8842 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -50,7 +50,7 @@
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
-.commit-box.gray-content-block.middle-block
+.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 58aa45e8d2c..02297158dec 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -2,7 +2,9 @@
- page_description @commit.description
= render "projects/commits/header_title"
-= render "commit_box"
+
+.prepend-top-default
+ = render "commit_box"
- if @ci_commit
= render "ci_menu"
- else
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
index 74a05df24d3..1736dccaf3c 100644
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ b/app/views/projects/commit_statuses/_commit_status.html.haml
@@ -66,8 +66,8 @@
%td
.pull-right
- - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
- = link_to commit_status.download_url, title: 'Download artifacts' do
+ - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts?
+ = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, commit_status.project)
- if commit_status.active?
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index fcccb002d7e..498c5e05b32 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 034057da42e..ede64d47ab3 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -6,7 +6,7 @@
= render "head"
-.gray-content-block
+.gray-content-block.second-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index f9d661d59d2..f67058ae0ba 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -3,7 +3,7 @@
- diff_files = safe_diff_files(diffs)
-.gray-content-block.middle-block.oneline-block
+.content-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 31e752c6649..8a99aceef7f 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,6 +1,6 @@
- @blank_container = true
-.project-edit-container
+.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
.panel.panel-default
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index a47643bd09c..79a56647c53 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index a14943b15d3..dd2c59e112a 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,7 +18,7 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 095876450a0..200bfa5ac4f 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -45,7 +45,7 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index d1db0f64f88..1142c584592 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -57,7 +57,7 @@
%span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone)
-%ul.center-top-menu.no-top.no-bottom
+%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 29225a36364..6239a148905 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -2,7 +2,7 @@
= render "header_title"
- @blank_container = true
-.project-members-page
+.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 315afe4a764..2d5b9f43c24 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -1,5 +1,6 @@
- page_title "Runners"
-.light
+
+.light.prepend-top-default
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 8436be433b1..4310f038fc9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
@@ -8,11 +10,10 @@
= render 'shared/no_password'
= render 'projects/last_push'
-
= render "home_panel"
.project-stats.gray-content-block.second-block
- %ul.nav.nav-pills
+ %ul.nav
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
@@ -57,15 +58,17 @@
= link_to add_contribution_guide_path(@project) do
Add Contribution guide
-- if @project.archived?
- .text-warning.center.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- Archived project! Repository is read-only
-
- if @repository.commit
.content-block.second-block.white
- = render 'projects/last_commit', commit: @repository.commit, project: @project
+ %div{ class: container_class }
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
+
+%div{ class: container_class }
+ - if @project.archived?
+ .text-warning.center.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ Archived project! Repository is read-only
-%div{class: "project-show-#{default_project_view}"}
- = render default_project_view
+ %div{class: "project-show-#{default_project_view}"}
+ = render default_project_view
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 1927883513a..558e6146ae9 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,6 +1,6 @@
%div.tree-content-holder
.table-holder
- %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index c57570afa09..91fb2a44594 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -5,13 +5,13 @@
= 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'
-.pull-right
+.tree-controls
= render 'projects/find_file_link'
- if can? current_user, :download_code, @project
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix
- .gray-content-block.top-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index e6e6ad5bc4b..69ba301e231 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -7,7 +7,7 @@
= render 'projects/wikis/new'
- %ul.center-top-menu
+ %ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index f0547e9c057..53b37b1104e 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -5,12 +5,9 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
- %p.hidden.text-danger{data: { error: "slug" }}
- The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
- %p.hint
- Please don't use spaces.
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
.form-actions
= link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 11c8c4f0eba..dd27ea2b11b 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -3,14 +3,12 @@
= render 'nav'
.gray-content-block
- .row
- .col-sm-6
- %h3.page-title.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .col-sm-6
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
.git-empty.prepend-top-default
%fieldset
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 481451edb23..2c3fca439f3 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-tabs.search-filter
+%ul.nav-links.search-filter
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 2a38c98dcfc..d0e64537621 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,7 +1,7 @@
- if @search_results.empty?
= render partial: "search/results/empty"
- else
- %p.light
+ .gray-content-block
Search results for
%code
= @search_term
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index f4f3dcfc29f..215dbb3909e 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -1,5 +1,7 @@
- page_title @search_term
-= render 'search/form'
+
+.prepend-top-default
+ = render 'search/form'
- if @search_term
= render 'search/category'
= render 'search/results'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 687a59c270f..faf7e49ed29 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,7 +1,7 @@
- project = project || @project
-.git-clone-holder
- .btn-group.clone-options
+.git-clone-holder.input-group
+ .input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span
= default_clone_protocol.upcase
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8495774accc..c38d9313dba 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-.btn-group.btn-group-next.event-filter
+%ul.nav-links.event-filter
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cbdecda4fff..f77feeb79cd 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,5 +1,5 @@
.milestones-filters
- %ul.center-top-menu
+ %ul.nav-links
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
= link_to milestones_filter_path(state: 'opened') do
Open
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 0e3e9275fc1..8d6f47b38ef 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,6 +1,6 @@
.issues-filters
.issues-state-filters
- %ul.center-top-menu
+ %ul.nav-links
- if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests'
- else
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 2299112bec7..9f4a7098ea2 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -69,15 +69,16 @@
You're not receiving notifications from this thread.
.subscribed{class: ( 'hidden' unless subscribed )}
You're receiving notifications because you're subscribed to this thread.
+
- project_ref = cross_project_reference(@project, issuable)
.block
.title
.cross-project-reference
- %span#cross-project-reference
+ %span
Reference:
- %a{href: '#', title:project_ref}
+ %cite{title: project_ref}
= project_ref
- = clipboard_button(clipboard_target: 'span#cross-project-reference')
+ = clipboard_button(clipboard_text: project_ref)
:javascript
new Subscription("#{toggle_subscription_path(issuable)}");
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index 4a84348ac82..83f61ce4b07 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 3c8ffb06648..9d4b0b2724c 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index ce17fc7bca1..849304ee2b6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,6 +1,7 @@
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
+- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
@@ -8,6 +9,25 @@
= render 'shared/show_aside'
.cover-block
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ %span.report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
+ = icon('rss')
+
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
@@ -47,74 +67,56 @@
= icon('map-marker')
= @user.location
+ %ul.nav-links.center
+ %li.active
+ = link_to "#activity", 'data-toggle' => 'tab' do
+ Activity
+ - if @groups.any?
+ %li
+ = link_to "#groups", 'data-toggle' => 'tab' do
+ Groups
+ - if @contributed_projects.present?
+ %li
+ = link_to "#contributed", 'data-toggle' => 'tab' do
+ Contributed projects
+ - if @projects.present?
+ %li
+ = link_to "#personal", 'data-toggle' => 'tab' do
+ Personal projects
- .cover-controls
- - if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
- = icon('pencil')
- - elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
- - if current_user
- &nbsp;
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
- = icon('rss')
-
-.gray-content-block.second-block
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
-
+%div{ class: container_class }
+ .tab-content
+ .tab-pane.active#activity
+ .gray-content-block.white.second-block
+ %div{ class: container_class }
+ .user-calendar
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
-%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @groups.any?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Groups
- - if @contributed_projects.present?
- %li
- = link_to "#contributed", 'data-toggle' => 'tab' do
- Contributed projects
- - if @projects.present?
- %li
- = link_to "#personal", 'data-toggle' => 'tab' do
- Personal projects
-.tab-content
- .tab-pane.active#activity
- .content_list
- = spinner
+ .content_list
+ = spinner
- - if @groups.any?
- .tab-pane#groups
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
+ - if @groups.any?
+ .tab-pane#groups
+ %ul.content-list
+ - @groups.each do |group|
+ = render 'shared/groups/group', group: group
- - if @contributed_projects.present?
- .tab-pane#contributed
- .contributed-projects
- = render 'shared/projects/list',
- projects: @contributed_projects.sort_by(&:star_count).reverse,
- projects_limit: 5, stars: true, avatar: true
+ - if @contributed_projects.present?
+ .tab-pane#contributed
+ .contributed-projects
+ = render 'shared/projects/list',
+ projects: @contributed_projects.sort_by(&:star_count).reverse,
+ projects_limit: 10, stars: true, avatar: true
- - if @projects.present?
- .tab-pane#personal
- .personal-projects
- = render 'shared/projects/list',
- projects: @projects.sort_by(&:star_count).reverse,
- projects_limit: 10, stars: true, avatar: true
+ - if @projects.present?
+ .tab-pane#personal
+ .personal-projects
+ = render 'shared/projects/list',
+ projects: @projects.sort_by(&:star_count).reverse,
+ projects_limit: 10, stars: true, avatar: true
:javascript
$(".user-calendar").load("#{user_calendar_path}");
diff --git a/config/routes.rb b/config/routes.rb
index 05d6ff1e884..75418db8d25 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -88,6 +88,12 @@ Rails.application.routes.draw do
end
end
+ resources :sent_notifications, only: [], constraints: { id: /\h{32}/ } do
+ member do
+ get :unsubscribe
+ end
+ end
+
# Spam reports
resources :abuse_reports, only: [:new, :create]
@@ -513,7 +519,7 @@ Rails.application.routes.draw do
end
end
- WIKI_SLUG_ID = { id: /[a-zA-Z.0-9_\-\/]+/ } unless defined? WIKI_SLUG_ID
+ WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
@@ -604,9 +610,14 @@ Rails.application.routes.draw do
member do
get :status
post :cancel
- get :download
post :retry
end
+
+ resource :artifacts, only: [] do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
+ end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
new file mode 100644
index 00000000000..03a12323845
--- /dev/null
+++ b/db/fixtures/development/14_builds.rb
@@ -0,0 +1,79 @@
+class Gitlab::Seeder::Builds
+ BUILD_STATUSES = %w(running pending success failed canceled)
+
+ def initialize(project)
+ @project = project
+ end
+
+ def seed!
+ ci_commits.each do |ci_commit|
+ build = Ci::Build.new(build_attributes_for(ci_commit))
+
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
+
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
+
+ begin
+ build.save!
+ print '.'
+ rescue ActiveRecord::RecordInvalid
+ print 'F'
+ end
+ end
+ end
+
+ def ci_commits
+ commits = @project.repository.commits('master', nil, 5)
+ commits_sha = commits.map { |commit| commit.raw.id }
+ commits_sha.map do |sha|
+ @project.ensure_ci_commit(sha)
+ end
+ rescue
+ []
+ end
+
+ def build_attributes_for(ci_commit)
+ { name: 'test build', commands: "$ build command",
+ stage: 'test', stage_idx: 1, ref: 'master',
+ user_id: build_user, gl_project_id: @project.id,
+ status: build_status, commit_id: ci_commit.id,
+ created_at: Time.now, updated_at: Time.now }
+ end
+
+ def build_user
+ @project.team.users.sample
+ end
+
+ def build_status
+ BUILD_STATUSES.sample
+ end
+
+ def artifacts_archive_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ end
+
+ def artifacts_metadata_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+
+ end
+
+ def artifacts_cache_file(file_path)
+ cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
+
+ FileUtils.copy(file_path, cache_path)
+ File.open(cache_path) do |file|
+ yield file
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ Project.all.sample(10).each do |project|
+ project_builds = Gitlab::Seeder::Builds.new(project)
+ project_builds.seed!
+ end
+end
diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
new file mode 100644
index 00000000000..6c282fc5039
--- /dev/null
+++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
@@ -0,0 +1,5 @@
+class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_metadata, :text
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 42c3e79f9d7..2fc8c4d5ed4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -123,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160113111034) do
t.string "description"
t.text "artifacts_file"
t.integer "gl_project_id"
+ t.text "artifacts_metadata"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
diff --git a/doc/api/README.md b/doc/api/README.md
index 9ab5f87069f..2fa177ff4dd 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,7 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
+- [Builds](builds.md)
- [Build triggers](build_triggers.md)
- [Build Variables](build_variables.md)
diff --git a/doc/api/builds.md b/doc/api/builds.md
new file mode 100644
index 00000000000..ecb50754c88
--- /dev/null
+++ b/doc/api/builds.md
@@ -0,0 +1,360 @@
+# Builds API
+
+## List project builds
+
+Get a list of builds in a project.
+
+```
+GET /projects/:id/builds
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+
+### Example of request
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
+```
+
+### Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## List commit builds
+
+Get a list of builds for specific commit in a project.
+
+```
+GET /projects/:id/repository/commits/:sha/builds
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| sha | string | yes | The SHA id of a commit |
+| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+
+### Example of request
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
+```
+
+### Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "download_url": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.957Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:33.913Z",
+ "id": 9,
+ "name": "brakeman",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:33.727Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## Get a single build
+
+Get a single build of a project
+
+```
+GET /projects/:id/builds/:build_id
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| build\_id | integer | yes | The ID of a build |
+
+### Example of request
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
+```
+
+### Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.880Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:31.198Z",
+ "id": 8,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:30.733Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+}
+```
+
+## Cancel a build
+
+Cancel a single build of a project
+
+```
+POST /projects/:id/builds/:build_id/cancel
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| build\_id | integer | yes | The ID of a build |
+
+### Example of request
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
+```
+
+### Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "download_url": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+}
+```
+
+## Retry a build
+
+Retry a single build of a project
+
+```
+POST /projects/:id/builds/:build_id/retry
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| build\_id | integer | yes | The ID of a build |
+
+### Example of request
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
+```
+
+### Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "download_url": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "pending",
+ "tag": false,
+ "user": null
+}
+```
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 86d205ba7a5..4cfb8402943 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -74,10 +74,11 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
-1. Reconfigure GitLab for the changes to take effect:
+1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh
sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart mailroom
```
1. Verify that everything is configured correctly:
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 18bf3db1744..787d21f7f8f 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -84,7 +84,12 @@ The instructions make the assumption that you will be using the email address `i
quit
```
- (Note: The `.` is a literal period on its own line)
+ _**Note:** The `.` is a literal period on its own line._
+
+ _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
+ then your Postfix `my_network` configuration is not correct. The error will
+ say 'Temporary lookup failure'. See
+ [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
1. Check if the `incoming` user received the email:
@@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
1. Test the new setup:
1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
- 2. Check if the `incoming` user received the email:
+ 1. Check if the `incoming` user received the email:
```sh
su - incoming
@@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
q
```
+ _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
+ version of `mail` doesn't support Maildir style mailboxes. Install
+ `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
+ try the above steps again, substituting `heirloom-mailx` for the `mail`
+ command._
+
1. Log out of the `incoming` account and go back to being `root`:
```sh
diff --git a/features/project/builds.feature b/features/project/builds.feature
new file mode 100644
index 00000000000..c00b0a7ae07
--- /dev/null
+++ b/features/project/builds.feature
@@ -0,0 +1,58 @@
+Feature: Project Builds
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And CI is enabled
+ And I have recent build for my project
+
+ Scenario: I browse build summary page
+ When I visit recent build summary page
+ Then I see summary for build
+ And I see build trace
+
+ Scenario: I download build artifacts
+ Given recent build has artifacts available
+ When I visit recent build summary page
+ And I click artifacts download button
+ Then download of build artifacts archive starts
+
+ Scenario: I browse build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build summary page
+ And I click artifacts browse button
+ Then I should see content of artifacts archive
+
+ Scenario: I browse subdirectory of build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I click link to subdirectory within build artifacts
+ Then I should see content of subdirectory within artifacts archive
+
+ Scenario: I browse directory with UTF-8 characters in name
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ And recent build artifacts contain directory with UTF-8 characters
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I navigate to directory with UTF-8 characters in name
+ Then I should see content of directory with UTF-8 characters in name
+
+ Scenario: I try to browse directory with invalid UTF-8 characters in name
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ And recent build artifacts contain directory with invalid UTF-8 characters
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I navigate to parent directory of directory with invalid name
+ Then I should not see directory with invalid name on the list
+
+ Scenario: I download a single file from build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I click download button for a file within build artifacts
+ Then download of a file extracted from build artifacts should start
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index af970ecf2d0..d4811b1ff54 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -70,11 +70,6 @@ Feature: Project Wiki
Then I should see non-escaped link in the pages list
@javascript
- Scenario: Creating an invalid new page
- Given I create a New page with an invalid name
- Then I should see an error message
-
- @javascript
Scenario: Edit Wiki page that has a path
Given I create a New page with paths
And I click on the "Pages" button
diff --git a/features/steps/project/builds.rb b/features/steps/project/builds.rb
new file mode 100644
index 00000000000..28395281077
--- /dev/null
+++ b/features/steps/project/builds.rb
@@ -0,0 +1,89 @@
+class Spinach::Features::ProjectBuilds < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedBuilds
+ include RepoHelpers
+
+ step 'I see summary for build' do
+ expect(page).to have_content "Build ##{@build.id}"
+ end
+
+ step 'I see build trace' do
+ expect(page).to have_css '#build-trace'
+ end
+
+ step 'I click artifacts download button' do
+ page.within('.artifacts') { click_link 'Download' }
+ end
+
+ step 'download of build artifacts archive starts' do
+ expect(page.response_headers['Content-Type']).to eq 'application/zip'
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
+ end
+
+ step 'I click artifacts browse button' do
+ page.within('.artifacts') { click_link 'Browse' }
+ end
+
+ step 'I should see content of artifacts archive' do
+ page.within('.tree-table') do
+ expect(page).to have_no_content '..'
+ expect(page).to have_content 'other_artifacts_0.1.2'
+ expect(page).to have_content 'ci_artifacts.txt'
+ expect(page).to have_content 'rails_sample.jpg'
+ end
+ end
+
+ step 'I click link to subdirectory within build artifacts' do
+ page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
+ end
+
+ step 'I should see content of subdirectory within artifacts archive' do
+ page.within('.tree-table') do
+ expect(page).to have_content '..'
+ expect(page).to have_content 'another-subdirectory'
+ expect(page).to have_content 'doc_sample.txt'
+ end
+ end
+
+ step 'recent build artifacts contain directory with UTF-8 characters' do
+ # metadata fixture contains relevant directory
+ end
+
+ step 'I navigate to directory with UTF-8 characters in name' do
+ page.within('.tree-table') { click_link 'tests_encoding' }
+ page.within('.tree-table') { click_link 'utf8 test dir ✓' }
+ end
+
+ step 'I should see content of directory with UTF-8 characters in name' do
+ page.within('.tree-table') do
+ expect(page).to have_content '..'
+ expect(page).to have_content 'regular_file_2'
+ end
+ end
+
+ step 'recent build artifacts contain directory with invalid UTF-8 characters' do
+ # metadata fixture contains relevant directory
+ end
+
+ step 'I navigate to parent directory of directory with invalid name' do
+ page.within('.tree-table') { click_link 'tests_encoding' }
+ end
+
+ step 'I should not see directory with invalid name on the list' do
+ page.within('.tree-table') do
+ expect(page).to have_no_content('non-utf8-dir')
+ end
+ end
+
+ step 'I click download button for a file within build artifacts' do
+ page.within('.tree-table') { first('.artifact-download').click }
+ end
+
+ step 'download of a file extracted from build artifacts should start' do
+ # this will be accelerated by Workhorse
+ response_json = JSON.parse(page.body, symbolize_names: true)
+ expect(response_json[:archive]).to end_with('build_artifacts.zip')
+ expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt')
+ end
+end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 91d227fadbf..d753ae14590 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -132,16 +132,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(current_path).to include 'one/two/three'
end
- step 'I create a New page with an invalid name' do
- click_on 'New Page'
- fill_in 'Page slug', with: 'invalid name'
- click_on 'Create Page'
- end
-
- step 'I should see an error message' do
- expect(page).to have_content "The page slug is invalid"
- end
-
step 'I should see non-escaped link in the pages list' do
expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index eb2ccd9d01e..0bee91d758d 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
- expect(find('div.content ul.center-top-menu li.active')).to have_content(content)
+ expect(find('div.content ul.nav-links li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1)
+ expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
end
step 'no other sub navs should be active' do
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
new file mode 100644
index 00000000000..a83d74e5946
--- /dev/null
+++ b/features/steps/shared/builds.rb
@@ -0,0 +1,28 @@
+module SharedBuilds
+ include Spinach::DSL
+
+ step 'CI is enabled' do
+ @project.enable_ci
+ end
+
+ step 'I have recent build for my project' do
+ ci_commit = create :ci_commit, project: @project, sha: sample_commit.id
+ @build = create :ci_build, commit: ci_commit
+ end
+
+ step 'I visit recent build summary page' do
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ step 'recent build has artifacts available' do
+ artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ archive = fixture_file_upload(artifacts, 'application/zip')
+ @build.update_attributes(artifacts_file: archive)
+ end
+
+ step 'recent build has artifacts metadata available' do
+ metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ gzip = fixture_file_upload(metadata, 'application/x-gzip')
+ @build.update_attributes(artifacts_metadata: gzip)
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 098dd975840..7efe0a0262f 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -54,6 +54,7 @@ module API
mount Keys
mount Tags
mount Triggers
+ mount Builds
mount Variables
end
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
new file mode 100644
index 00000000000..d293f988165
--- /dev/null
+++ b/lib/api/builds.rb
@@ -0,0 +1,149 @@
+module API
+ # Projects builds API
+ class Builds < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Get a project builds
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
+ # if none provided showing all builds)
+ # Example Request:
+ # GET /projects/:id/builds
+ get ':id/builds' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Get builds for a specific commit of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # sha (required) - The SHA id of a commit
+ # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
+ # if none provided showing all builds)
+ # Example Request:
+ # GET /projects/:id/repository/commits/:sha/builds
+ get ':id/repository/commits/:sha/builds' do
+ commit = user_project.ci_commits.find_by_sha(params[:sha])
+ return not_found! unless commit
+
+ builds = commit.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Get a specific build of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # GET /projects/:id/builds/:build_id
+ get ':id/builds/:build_id' do
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Get a trace of a specific build of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # GET /projects/:id/build/:build_id/trace
+ #
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ get ':id/builds/:build_id/trace' do
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ # Cancel a specific build of a project
+ #
+ # parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example request:
+ # post /projects/:id/build/:build_id/cancel
+ post ':id/builds/:build_id/cancel' do
+ authorize_manage_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ build.cancel
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Retry a specific build of a project
+ #
+ # parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example request:
+ # post /projects/:id/build/:build_id/retry
+ post ':id/builds/:build_id/retry' do
+ authorize_manage_builds!
+
+ build = get_build(params[:build_id])
+ return forbidden!('Build is not retryable') unless build && build.retryable?
+
+ build = Ci::Build.retry(build)
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+ scope =
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_manage_builds!
+ authorize! :manage_builds, user_project
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index d3803f4ec70..82a75734de0 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -367,6 +367,33 @@ module API
expose :id, :variables
end
+ class Runner < Grape::Entity
+ expose :id
+ expose :description
+ expose :active
+ expose :is_shared
+ expose :name
+ end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: User
+ # TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API
+ # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255)
+ expose :download_url do |repo_obj, options|
+ if options[:user_can_download_artifacts]
+ repo_obj.download_url
+ end
+ end
+ expose :commit, with: RepoCommit do |repo_obj, _options|
+ if repo_obj.respond_to?(:commit)
+ repo_obj.commit.commit_data
+ end
+ end
+ expose :runner, with: Runner
+ end
+
class Trigger < Grape::Entity
expose :token, :created_at, :updated_at, :deleted_at, :last_used
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a4df810e755..6d2380cf47d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -97,11 +97,9 @@ module API
end
def paginate(relation)
- per_page = params[:per_page].to_i
- paginated = relation.page(params[:page]).per(per_page)
- add_pagination_headers(paginated, per_page)
-
- paginated
+ relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ add_pagination_headers(data)
+ end
end
def authenticate!
@@ -289,12 +287,14 @@ module API
# file helpers
- def uploaded_file!(field, uploads_path)
+ def uploaded_file(field, uploads_path)
if params[field]
bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
return params[field]
end
+ return nil unless params["#{field}.path"] && params["#{field}.name"]
+
# sanitize file paths
# this requires all paths to exist
required_attributes! %W(#{field}.path)
@@ -327,16 +327,26 @@ module API
private
- def add_pagination_headers(paginated, per_page)
+ def add_pagination_headers(paginated_data)
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+ end
+
+ def pagination_links(paginated_data)
request_url = request.url.split('?').first
links = []
- links << %(<#{request_url}?page=#{paginated.current_page - 1}&per_page=#{per_page}>; rel="prev") unless paginated.first_page?
- links << %(<#{request_url}?page=#{paginated.current_page + 1}&per_page=#{per_page}>; rel="next") unless paginated.last_page?
- links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first")
- links << %(<#{request_url}?page=#{paginated.total_pages}&per_page=#{per_page}>; rel="last")
+ links << %(<#{request_url}?page=#{paginated_data.current_page - 1}&per_page=#{paginated_data.limit_value}>; rel="prev") unless paginated_data.first_page?
+ links << %(<#{request_url}?page=#{paginated_data.current_page + 1}&per_page=#{paginated_data.limit_value}>; rel="next") unless paginated_data.last_page?
+ links << %(<#{request_url}?page=1&per_page=#{paginated_data.limit_value}>; rel="first")
+ links << %(<#{request_url}?page=#{paginated_data.total_pages}&per_page=#{paginated_data.limit_value}>; rel="last")
- header 'Link', links.join(', ')
+ links.join(', ')
end
def abilities
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index ba2866e1efa..0257848b6bc 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
# Common methods for ReferenceFilters that support an optional cross-project
# reference.
diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb
index fd4fe024252..905c4c0144e 100644
--- a/lib/banzai/filter.rb
+++ b/lib/banzai/filter.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/string/output_safety'
-require 'banzai'
module Banzai
module Filter
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index b2db10e6864..cdbaecf8d90 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# Issues, Merge Requests, Snippets, Commits and Commit Ranges share
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index da4ee80c1b5..856f56fb175 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index e67cd45ab9b..470727ee312 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces commit range references with links.
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 9e57608b483..713a56ba949 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces commit references with links.
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 86838e1483c..5952a031626 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,5 +1,4 @@
require 'action_controller'
-require 'banzai'
require 'gitlab_emoji'
require 'html/pipeline/filter'
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 6136e73c096..edc26386903 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces external issue tracker references with links.
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index ac87b9820af..8d368f3b9e7 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
new file mode 100644
index 00000000000..fe01dae4850
--- /dev/null
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -0,0 +1,151 @@
+require 'banzai'
+require 'html/pipeline/filter'
+
+module Banzai
+ module Filter
+ # HTML Filter for parsing Gollum's tags in HTML. It's only parses the
+ # following tags:
+ #
+ # - Link to internal pages:
+ #
+ # * [[Bug Reports]]
+ # * [[How to Contribute|Contributing]]
+ #
+ # - Link to external resources:
+ #
+ # * [[http://en.wikipedia.org/wiki/Git_(software)]]
+ # * [[Git|http://en.wikipedia.org/wiki/Git_(software)]]
+ #
+ # - Link internal images, the special attributes will be ignored:
+ #
+ # * [[images/logo.png]]
+ # * [[images/logo.png|alt=Logo]]
+ #
+ # - Link external images, the special attributes will be ignored:
+ #
+ # * [[http://example.com/images/logo.png]]
+ # * [[http://example.com/images/logo.png|alt=Logo]]
+ #
+ # Based on Gollum::Filter::Tags
+ #
+ # Context options:
+ # :project_wiki (required) - Current project wiki.
+ #
+ class GollumTagsFilter < HTML::Pipeline::Filter
+ include ActionView::Helpers::TagHelper
+
+ # Pattern to match tags content that should be parsed in HTML.
+ #
+ # Gollum's tags have been made to resemble the tags of other markups,
+ # especially MediaWiki. The basic syntax is:
+ #
+ # [[tag]]
+ #
+ # Some tags will accept attributes which are separated by pipe
+ # symbols.Some attributes must precede the tag and some must follow it:
+ #
+ # [[prefix-attribute|tag]]
+ # [[tag|suffix-attribute]]
+ #
+ # See https://github.com/gollum/gollum/wiki
+ #
+ # Rubular: http://rubular.com/r/7dQnE5CUCH
+ TAGS_PATTERN = %r{\[\[(.+?)\]\]}
+
+ # Pattern to match allowed image extensions
+ ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i
+
+ def call
+ search_text_nodes(doc).each do |node|
+ content = node.content
+
+ next unless content.match(TAGS_PATTERN)
+
+ html = process_tag($1)
+
+ if html && html != node.content
+ node.replace(html)
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ # Process a single tag into its final HTML form.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML version of the tag.
+ def process_tag(tag)
+ parts = tag.split('|')
+
+ return if parts.size.zero?
+
+ process_image_tag(parts) || process_page_link_tag(parts)
+ end
+
+ # Attempt to process the tag as an image tag.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML if the tag is a valid image tag or nil
+ # if it is not.
+ def process_image_tag(parts)
+ content = parts[0].strip
+
+ return unless image?(content)
+
+ if url?(content)
+ path = content
+ elsif file = project_wiki.find_file(content)
+ path = ::File.join project_wiki_base_path, file.path
+ end
+
+ if path
+ content_tag(:img, nil, src: path)
+ end
+ end
+
+ def image?(path)
+ path =~ ALLOWED_IMAGE_EXTENSIONS
+ end
+
+ def url?(path)
+ path.start_with?(*%w(http https))
+ end
+
+ # Attempt to process the tag as a page link tag.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML if the tag is a valid page link tag or nil
+ # if it is not.
+ def process_page_link_tag(parts)
+ if parts.size == 1
+ url = parts[0].strip
+ else
+ name, url = *parts.compact.map(&:strip)
+ end
+
+ content_tag(:a, name || url, href: url)
+ end
+
+ def project_wiki
+ context[:project_wiki]
+ end
+
+ def project_wiki_base_path
+ project_wiki && project_wiki.wiki_base_path
+ end
+
+ # Ensure that a :project_wiki key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project_wiki
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 51180cb901a..9f08aa36e8b 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces issue references with links. References to
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index a3a7a23c1e6..95e7d209119 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces label references with links.
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index d09cf41df39..0659fed1419 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 755b946a34b..57c71708992 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces merge request references with links. References
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index 66f77902319..7141ed7c9bd 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 5dd6d2fe3c7..20bd4f7ee6e 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/string/output_safety'
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
index bef04112919..86d484feb90 100644
--- a/lib/banzai/filter/reference_gatherer_filter.rb
+++ b/lib/banzai/filter/reference_gatherer_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 66f166939e4..41380627d39 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index d03e3ae4b3c..3f49d492f2f 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter'
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index 1ad5df96f85..c870a42f741 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces snippet references with links. References to
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index c889cc1e97c..8c5855e5ffc 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 9b3e67206d5..4056dcd6d64 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index d0ce13003a5..66608c9859c 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'task_list/filter'
module Banzai
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 1a1d0aad8ca..f642aee0967 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 964ab60f614..24f16f8b547 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces user or group references with links.
diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb
index 073ec5d9801..1095b4debc7 100644
--- a/lib/banzai/lazy_reference.rb
+++ b/lib/banzai/lazy_reference.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
class LazyReference
def self.load(refs)
diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb
index 4e017809d9d..142a9962eb1 100644
--- a/lib/banzai/pipeline.rb
+++ b/lib/banzai/pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
def self.[](name)
diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb
index 5e76a817be5..f1331c0ebf9 100644
--- a/lib/banzai/pipeline/asciidoc_pipeline.rb
+++ b/lib/banzai/pipeline/asciidoc_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class AsciidocPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb
index 957f352aec5..9694e4bc23f 100644
--- a/lib/banzai/pipeline/atom_pipeline.rb
+++ b/lib/banzai/pipeline/atom_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class AtomPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb
index cd30009e5c0..db5177db7b3 100644
--- a/lib/banzai/pipeline/base_pipeline.rb
+++ b/lib/banzai/pipeline/base_pipeline.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline'
module Banzai
diff --git a/lib/banzai/pipeline/combined_pipeline.rb b/lib/banzai/pipeline/combined_pipeline.rb
index f3bf1809d18..9485199132e 100644
--- a/lib/banzai/pipeline/combined_pipeline.rb
+++ b/lib/banzai/pipeline/combined_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
module CombinedPipeline
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
index 94c2cb165a5..20e24ace352 100644
--- a/lib/banzai/pipeline/description_pipeline.rb
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class DescriptionPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb
index 14356145a35..e47c384afc1 100644
--- a/lib/banzai/pipeline/email_pipeline.rb
+++ b/lib/banzai/pipeline/email_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class EmailPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/full_pipeline.rb b/lib/banzai/pipeline/full_pipeline.rb
index 72395a5d50e..d47ddfda4be 100644
--- a/lib/banzai/pipeline/full_pipeline.rb
+++ b/lib/banzai/pipeline/full_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 838155e8831..b7a38ea8427 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/note_pipeline.rb b/lib/banzai/pipeline/note_pipeline.rb
index 89335143852..7890f20f716 100644
--- a/lib/banzai/pipeline/note_pipeline.rb
+++ b/lib/banzai/pipeline/note_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class NotePipeline < FullPipeline
diff --git a/lib/banzai/pipeline/plain_markdown_pipeline.rb b/lib/banzai/pipeline/plain_markdown_pipeline.rb
index 998fd75daa2..3fbc681457b 100644
--- a/lib/banzai/pipeline/plain_markdown_pipeline.rb
+++ b/lib/banzai/pipeline/plain_markdown_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class PlainMarkdownPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 148f24b6ce1..bd338c045f3 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class PostProcessPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb
index 4f9bc9fcccc..eaddccd30a5 100644
--- a/lib/banzai/pipeline/reference_extraction_pipeline.rb
+++ b/lib/banzai/pipeline/reference_extraction_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class ReferenceExtractionPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index a3c9d4f43aa..8b84ab401df 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class SingleLinePipeline < GfmPipeline
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
new file mode 100644
index 00000000000..50b5450e70b
--- /dev/null
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -0,0 +1,11 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class WikiPipeline < FullPipeline
+ def self.filters
+ super.insert(1, Filter::GollumTagsFilter)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index 2c197d31898..f4079538ec5 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 15faa6edd84..fb87637b94f 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -78,11 +78,13 @@ module Ci
# Parameters:
# id (required) - The ID of a build
# token (required) - The build authorization token
- # file (required) - The uploaded file
+ # file (required) - Artifacts file
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
# file.type - real content type as send in Content-Type
+ # metadata.path - path to locally stored body (generated by Workhorse)
+ # metadata.name - filename (generated by Workhorse)
# Headers:
# BUILD-TOKEN (required) - The build authorization token, the same as token
# Body:
@@ -96,13 +98,20 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
authenticate_build_token!(build)
- forbidden!('build is not running') unless build.running?
+ forbidden!('Build is not running!') unless build.running?
- file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
- file_to_large! unless file.size < max_artifacts_size
+ artifacts_upload_path = ArtifactUploader.artifacts_upload_path
+ artifacts = uploaded_file(:file, artifacts_upload_path)
+ metadata = uploaded_file(:metadata, artifacts_upload_path)
- if build.update_attributes(artifacts_file: file)
- present build, with: Entities::Build
+ bad_request!('Missing artifacts file!') unless artifacts
+ file_to_large! unless artifacts.size < max_artifacts_size
+
+ build.artifacts_file = artifacts
+ build.artifacts_metadata = metadata
+
+ if build.save
+ present(build, with: Entities::Build)
else
render_validation_error!(build)
end
@@ -148,6 +157,7 @@ module Ci
not_found! unless build
authenticate_build_token!(build)
build.remove_artifacts_file!
+ build.remove_artifacts_metadata!
end
end
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
new file mode 100644
index 00000000000..1344f5d120b
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -0,0 +1,109 @@
+require 'zlib'
+require 'json'
+
+module Gitlab
+ module Ci
+ module Build
+ module Artifacts
+ class Metadata
+ class ParserError < StandardError; end
+
+ VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
+ INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
+
+ attr_reader :file, :path, :full_version
+
+ def initialize(file, path)
+ @file, @path = file, path
+ @full_version = read_version
+ end
+
+ def version
+ @full_version.match(VERSION_PATTERN)[1]
+ end
+
+ def errors
+ gzip do |gz|
+ read_string(gz) # version
+ errors = read_string(gz)
+ raise ParserError, 'Errors field not found!' unless errors
+
+ begin
+ JSON.parse(errors)
+ rescue JSON::ParserError
+ raise ParserError, 'Invalid errors field!'
+ end
+ end
+ end
+
+ def find_entries!
+ gzip do |gz|
+ 2.times { read_string(gz) } # version and errors fields
+ match_entries(gz)
+ end
+ end
+
+ def to_entry
+ entries = find_entries!
+ Entry.new(@path, entries)
+ end
+
+ private
+
+ def match_entries(gz)
+ entries = {}
+ match_pattern = %r{^#{Regexp.escape(@path)}[^/]*/?$}
+
+ until gz.eof? do
+ begin
+ path = read_string(gz).force_encoding('UTF-8')
+ meta = read_string(gz).force_encoding('UTF-8')
+
+ next unless path.valid_encoding? && meta.valid_encoding?
+ next unless path =~ match_pattern
+ next if path =~ INVALID_PATH_PATTERN
+
+ entries[path] = JSON.parse(meta, symbolize_names: true)
+ rescue JSON::ParserError, Encoding::CompatibilityError
+ next
+ end
+ end
+
+ entries
+ end
+
+ def read_version
+ gzip do |gz|
+ version_string = read_string(gz)
+
+ unless version_string
+ raise ParserError, 'Artifacts metadata file empty!'
+ end
+
+ unless version_string =~ VERSION_PATTERN
+ raise ParserError, 'Invalid version!'
+ end
+
+ version_string.chomp
+ end
+ end
+
+ def read_uint32(gz)
+ binary = gz.read(4)
+ binary.unpack('L>')[0] if binary
+ end
+
+ def read_string(gz)
+ string_size = read_uint32(gz)
+ return nil unless string_size
+ gz.read(string_size)
+ end
+
+ def gzip(&block)
+ Zlib::GzipReader.open(@file, &block)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
new file mode 100644
index 00000000000..25b71fc3275
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -0,0 +1,119 @@
+module Gitlab
+ module Ci::Build::Artifacts
+ class Metadata
+ ##
+ # Class that represents an entry (path and metadata) to a file or
+ # directory in GitLab CI Build Artifacts binary file / archive
+ #
+ # This is IO-operations safe class, that does similar job to
+ # Ruby's Pathname but without the risk of accessing filesystem.
+ #
+ # This class is working only with UTF-8 encoded paths.
+ #
+ class Entry
+ attr_reader :path, :entries
+ attr_accessor :name
+
+ def initialize(path, entries)
+ @path = path.dup.force_encoding('UTF-8')
+ @entries = entries
+
+ if path.include?("\0")
+ raise ArgumentError, 'Path contains zero byte character!'
+ end
+
+ unless path.valid_encoding?
+ raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
+ end
+ end
+
+ def directory?
+ blank_node? || @path.end_with?('/')
+ end
+
+ def file?
+ !directory?
+ end
+
+ def has_parent?
+ nodes > 0
+ end
+
+ def parent
+ return nil unless has_parent?
+ self.class.new(@path.chomp(basename), @entries)
+ end
+
+ def basename
+ (directory? && !blank_node?) ? name + '/' : name
+ end
+
+ def name
+ @name || @path.split('/').last.to_s
+ end
+
+ def children
+ return [] unless directory?
+ return @children if @children
+
+ child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$}
+ @children = select_entries { |path| path =~ child_pattern }
+ end
+
+ def directories(opts = {})
+ return [] unless directory?
+ dirs = children.select(&:directory?)
+ return dirs unless has_parent? && opts[:parent]
+
+ dotted_parent = parent
+ dotted_parent.name = '..'
+ dirs.prepend(dotted_parent)
+ end
+
+ def files
+ return [] unless directory?
+ children.select(&:file?)
+ end
+
+ def metadata
+ @entries[@path] || {}
+ end
+
+ def nodes
+ @path.count('/') + (file? ? 1 : 0)
+ end
+
+ def blank_node?
+ @path.empty? # "" is considered to be './'
+ end
+
+ def exists?
+ blank_node? || @entries.include?(@path)
+ end
+
+ def empty?
+ children.empty?
+ end
+
+ def to_s
+ @path
+ end
+
+ def ==(other)
+ @path == other.path && @entries == other.entries
+ end
+
+ def inspect
+ "#{self.class.name}: #{@path}"
+ end
+
+ private
+
+ def select_entries
+ selected = @entries.select { |path, _metadata| yield path }
+ selected.map { |path, _metadata| self.class.new(path, @entries) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 7015fe36c3d..516e59b87a3 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -56,8 +56,9 @@ module Gitlab
private
def filename?(line)
- line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
- '--- /tmp/diffy', '+++ /tmp/diffy')
+ line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
+ '+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit
+ '--- /tmp/diffy', '+++ /tmp/diffy')
end
def identification_type(line)
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 2b0afbc7b39..18929b9113b 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class Importer
+ include Gitlab::ShellAdapter
+
attr_reader :project, :client
def initialize(project)
@@ -12,10 +14,7 @@ module Gitlab
end
def execute
- import_issues
- import_pull_requests
-
- true
+ import_issues && import_pull_requests && import_wiki
end
private
@@ -34,6 +33,10 @@ module Gitlab
end
end
end
+
+ true
+ rescue ActiveRecord::RecordInvalid
+ false
end
def import_pull_requests
@@ -48,6 +51,10 @@ module Gitlab
import_comments_on_diff(pull_request.number, merge_request)
end
end
+
+ true
+ rescue ActiveRecord::RecordInvalid
+ false
end
def import_comments(issue_number, noteable)
@@ -66,6 +73,18 @@ module Gitlab
noteable.notes.create!(comment.attributes)
end
end
+
+ def import_wiki
+ unless project.wiki_enabled?
+ wiki = WikiFormatter.new(project)
+ gitlab_shell.import_repository(wiki.path_with_namespace, wiki.import_url)
+ project.update_attribute(:wiki_enabled, true)
+ end
+
+ true
+ rescue Gitlab::Shell::Error
+ false
+ end
end
end
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 8c27ebd1ce8..474927069a5 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -20,7 +20,8 @@ module Gitlab
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "github",
import_source: repo.full_name,
- import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
+ import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
+ wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
).execute
project.create_import_data(data: { "github_session" => session_data } )
diff --git a/lib/gitlab/github_import/wiki_formatter.rb b/lib/gitlab/github_import/wiki_formatter.rb
new file mode 100644
index 00000000000..6c592ff469c
--- /dev/null
+++ b/lib/gitlab/github_import/wiki_formatter.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module GithubImport
+ class WikiFormatter
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def path_with_namespace
+ "#{project.path_with_namespace}.wiki"
+ end
+
+ def import_url
+ project.import_url.sub(/\.git\z/, ".wiki.git")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
index 8f3f43c0e91..699d8b9fc07 100644
--- a/lib/gitlab/markdown/pipeline.rb
+++ b/lib/gitlab/markdown/pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Gitlab
module Markdown
class Pipeline
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 4164e998dd1..4d830aa45e1 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
diff --git a/spec/controllers/sent_notification_controller_spec.rb b/spec/controllers/sent_notification_controller_spec.rb
new file mode 100644
index 00000000000..9ced397bd4a
--- /dev/null
+++ b/spec/controllers/sent_notification_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+describe SentNotificationsController, type: :controller do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, author: user) }
+ let(:sent_notification) { create(:sent_notification, noteable: issue) }
+
+ describe 'GET #unsubscribe' do
+ it 'returns a 404 when calling without existing id' do
+ get(:unsubscribe, id: '0' * 32)
+
+ expect(response.status).to be 404
+ end
+
+ context 'calling with id' do
+ it 'shows a flash message to the user' do
+ get(:unsubscribe, id: sent_notification.reply_key)
+
+ expect(response.status).to be 302
+
+ expect(response).to redirect_to new_user_session_path
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index d6b4efa9a03..2a81684dfcf 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -212,4 +212,11 @@ FactoryGirl.define do
provider 'ldapmain'
extern_uid 'my-ldap-id'
end
+
+ factory :sent_notification do
+ project
+ recipient factory: :user
+ noteable factory: :issue
+ reply_key "0123456789abcdef" * 2
+ end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index f76e826f138..d2db77f6286 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -30,6 +30,7 @@ FactoryGirl.define do
name 'test'
ref 'master'
tag false
+ created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
@@ -42,6 +43,10 @@ FactoryGirl.define do
commit factory: :ci_commit
+ trait :canceled do
+ status 'canceled'
+ end
+
after(:build) do |build, evaluator|
build.project = build.commit.project
end
@@ -54,5 +59,11 @@ FactoryGirl.define do
factory :ci_build_tag do
tag true
end
+
+ factory :ci_build_with_trace do
+ after(:create) do |build, evaluator|
+ build.trace = 'BUILD TRACE'
+ end
+ end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 240e56839df..d37bd103714 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -80,7 +80,11 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
- it { expect(page).to have_content 'Download artifacts' }
+ it 'has button to download artifacts' do
+ page.within('.artifacts') do
+ expect(page).to have_content 'Download'
+ end
+ end
end
end
@@ -111,7 +115,7 @@ describe "Builds" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link 'Download artifacts'
+ page.within('.artifacts') { click_link 'Download' }
end
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index e836d81c40b..12fd8d37210 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do
end
end
- context 'default pipeline' do
- before(:all) do
- @feat = MarkdownFeature.new
+ before(:all) do
+ @feat = MarkdownFeature.new
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
+ end
+ context 'default pipeline' do
+ before(:all) do
@html = markdown(@feat.raw_markdown)
end
@@ -221,6 +223,57 @@ describe 'GitLab Markdown', feature: true do
end
end
+ context 'wiki pipeline' do
+ before do
+ @project_wiki = @feat.project_wiki
+
+ file = Gollum::File.new(@project_wiki.wiki)
+ expect(file).to receive(:path).and_return('images/example.jpg')
+ expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
+
+ @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
+ end
+
+ it_behaves_like 'all pipelines'
+
+ it 'includes RelativeLinkFilter' do
+ expect(doc).not_to parse_relative_links
+ end
+
+ it 'includes EmojiFilter' do
+ expect(doc).to parse_emoji
+ end
+
+ it 'includes TableOfContentsFilter' do
+ expect(doc).to create_header_links
+ end
+
+ it 'includes AutolinkFilter' do
+ expect(doc).to create_autolinks
+ end
+
+ it 'includes all reference filters' do
+ aggregate_failures do
+ expect(doc).to reference_users
+ expect(doc).to reference_issues
+ expect(doc).to reference_merge_requests
+ expect(doc).to reference_snippets
+ expect(doc).to reference_commit_ranges
+ expect(doc).to reference_commits
+ expect(doc).to reference_labels
+ expect(doc).to reference_milestones
+ end
+ end
+
+ it 'includes TaskListFilter' do
+ expect(doc).to parse_task_lists
+ end
+
+ it 'includes GollumTagsFilter' do
+ expect(doc).to parse_gollum_tags
+ end
+ end
+
# Fake a `current_user` helper
def current_user
@feat.user
diff --git a/spec/fixtures/ci_build_artifacts.zip b/spec/fixtures/ci_build_artifacts.zip
new file mode 100644
index 00000000000..dae976d918e
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts.zip
Binary files differ
diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz
new file mode 100644
index 00000000000..fe9d4c8c661
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts_metadata.gz
Binary files differ
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 0620096d689..fe6d42acee2 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -230,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [ ] Incomplete sub-task 2
- [x] Complete sub-task 1
- [X] Complete task 2
+
+#### Gollum Tags
+
+- [[linked-resource]]
+- [[link-text|linked-resource]]
+- [[http://example.com]]
+- [[link-text|http://example.com/pdfs/gollum.pdf]]
+- [[images/example.jpg]]
+- [[http://example.com/images/example.jpg]]
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 762ec25c4f5..9a05b21335c 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
+ helper.instance_variable_set(:@project_wiki, @wiki)
end
- it "should use GitLab Flavored Markdown for markdown files" do
+ it "should use Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown).with('wiki content')
+ expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki)
helper.render_wiki_content(@wiki)
end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
new file mode 100644
index 00000000000..38baa819957
--- /dev/null
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Banzai::Filter::GollumTagsFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:user) { double }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+
+ describe 'validation' do
+ it 'ensure that a :project_wiki key exists in context' do
+ expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :project_wiki"
+ end
+ end
+
+ context 'linking internal images' do
+ it 'creates img tag if image exists' do
+ file = Gollum::File.new(project_wiki.wiki)
+ expect(file).to receive(:path).and_return('images/image.jpg')
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file)
+
+ tag = '[[images/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
+ end
+
+ it 'does not creates img tag if image does not exist' do
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(nil)
+
+ tag = '[[images/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.css('img').size).to eq 0
+ end
+ end
+
+ context 'linking external images' do
+ it 'creates img tag for valid URL' do
+ tag = '[[http://example.com/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg"
+ end
+
+ it 'does not creates img tag for invalid URL' do
+ tag = '[[http://example.com/image.pdf]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.css('img').size).to eq 0
+ end
+ end
+
+ context 'linking external resources' do
+ it "the created link's text will be equal to the resource's text" do
+ tag = '[[http://example.com]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'http://example.com'
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it "the created link's text will be link-text" do
+ tag = '[[link-text|http://example.com/pdfs/gollum.pdf]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'link-text'
+ expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf'
+ end
+ end
+
+ context 'linking internal resources' do
+ it "the created link's text will be equal to the resource's text" do
+ tag = '[[wiki-slug]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ end
+
+ it "the created link's text will be link-text" do
+ tag = '[[link-text|wiki-slug]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'link-text'
+ expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ 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
new file mode 100644
index 00000000000..41257103ead
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
+ let(:entries) do
+ { 'path/' => {},
+ 'path/dir_1/' => {},
+ 'path/dir_1/file_1' => {},
+ 'path/dir_1/file_b' => {},
+ 'path/dir_1/subdir/' => {},
+ 'path/dir_1/subdir/subfile' => {},
+ 'path/second_dir' => {},
+ 'path/second_dir/dir_3/file_2' => {},
+ 'path/second_dir/dir_3/file_3'=> {},
+ 'another_directory/'=> {},
+ 'another_file' => {},
+ '/file/with/absolute_path' => {} }
+ end
+
+ def path(example)
+ entry(example.metadata[:path])
+ end
+
+ def entry(path)
+ described_class.new(path, entries)
+ end
+
+ describe '/file/with/absolute_path', path: '/file/with/absolute_path' do
+ subject { |example| path(example) }
+
+ it { is_expected.to be_file }
+ it { is_expected.to have_parent }
+
+ describe '#basename' do
+ subject { |example| path(example).basename }
+ it { is_expected.to eq 'absolute_path' }
+ end
+ end
+
+ describe 'path/dir_1/', path: 'path/dir_1/' do
+ subject { |example| path(example) }
+ it { is_expected.to have_parent }
+ it { is_expected.to be_directory }
+
+ describe '#basename' do
+ subject { |example| path(example).basename }
+ it { is_expected.to eq 'dir_1/' }
+ end
+
+ describe '#name' do
+ subject { |example| path(example).name }
+ it { is_expected.to eq 'dir_1' }
+ end
+
+ describe '#parent' do
+ subject { |example| path(example).parent }
+ it { is_expected.to eq entry('path/') }
+ end
+
+ describe '#children' do
+ subject { |example| path(example).children }
+
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b'),
+ entry('path/dir_1/subdir/')
+ end
+ end
+
+ describe '#files' do
+ subject { |example| path(example).files }
+
+ it { is_expected.to all(be_file) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b')
+ end
+ end
+
+ describe '#directories' do
+ context 'without options' do
+ subject { |example| path(example).directories }
+
+ it { is_expected.to all(be_directory) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it { is_expected.to contain_exactly entry('path/dir_1/subdir/') }
+ end
+
+ context 'with option parent: true' do
+ subject { |example| path(example).directories(parent: true) }
+
+ it { is_expected.to all(be_directory) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/subdir/'),
+ entry('path/')
+ end
+ end
+
+ describe '#nodes' do
+ subject { |example| path(example).nodes }
+ it { is_expected.to eq 2 }
+ end
+
+ describe '#exists?' do
+ subject { |example| path(example).exists? }
+ it { is_expected.to be true }
+ end
+
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be false }
+ end
+ end
+ end
+
+ describe 'empty path', path: '' do
+ subject { |example| path(example) }
+ it { is_expected.to_not have_parent }
+
+ describe '#children' do
+ subject { |example| path(example).children }
+ it { expect(subject.count).to eq 3 }
+ end
+
+ end
+
+ describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do
+ describe '#nodes' do
+ subject { |example| path(example).nodes }
+ it { is_expected.to eq 4 }
+ end
+ end
+
+ describe 'non-existent/', path: 'non-existent/' do
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be true }
+ end
+
+ describe '#exists?' do
+ subject { |example| path(example).exists? }
+ it { is_expected.to be false }
+ end
+ end
+
+ describe 'another_directory/', path: 'another_directory/' do
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be true }
+ end
+ end
+
+ describe '#metadata' do
+ let(:entries) do
+ { 'path/' => { name: '/path/' },
+ 'path/file1' => { name: '/path/file1' },
+ 'path/file2' => { name: '/path/file2' } }
+ end
+
+ subject do
+ described_class.new('path/file1', entries).metadata[:name]
+ end
+
+ it { is_expected.to eq '/path/file1' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
new file mode 100644
index 00000000000..828eedfa7b0
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Metadata do
+ def metadata(path = '')
+ described_class.new(metadata_file_path, path)
+ end
+
+ let(:metadata_file_path) do
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ end
+
+ context 'metadata file exists' do
+ describe '#find_entries! empty string' do
+ subject { metadata('').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).to contain_exactly 'ci_artifacts.txt',
+ 'other_artifacts_0.1.2/',
+ 'rails_sample.jpg',
+ 'tests_encoding/'
+ end
+
+ it 'matches metadata for every path' do
+ expect(subject.keys.count).to eq 4
+ end
+
+ it 'return Hashes for each metadata' do
+ expect(subject.values).to all(be_kind_of(Hash))
+ end
+ end
+
+ describe '#find_entries! other_artifacts_0.1.2/' do
+ subject { metadata('other_artifacts_0.1.2/').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/',
+ 'other_artifacts_0.1.2/doc_sample.txt',
+ 'other_artifacts_0.1.2/another-subdirectory/'
+ end
+ end
+
+ describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do
+ subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/',
+ 'other_artifacts_0.1.2/another-subdirectory/empty_directory/',
+ 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
+ end
+ end
+
+ describe '#to_entry' do
+ subject { metadata('').to_entry }
+ it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) }
+ end
+
+ describe '#full_version' do
+ subject { metadata('').full_version }
+ it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' }
+ end
+
+ describe '#version' do
+ subject { metadata('').version }
+ it { is_expected.to eq '0.0.1' }
+ end
+
+ describe '#errors' do
+ subject { metadata('').errors }
+ it { is_expected.to eq({}) }
+ end
+ end
+
+ context 'metadata file does not exist' do
+ let(:metadata_file_path) { '' }
+
+ describe '#find_entries!' do
+ it 'raises error' do
+ expect { metadata.find_entries! }.to raise_error(Errno::ENOENT)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
new file mode 100644
index 00000000000..aed2aa39e3a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::WikiFormatter, lib: true do
+ let(:project) do
+ create(:project, namespace: create(:namespace, path: 'gitlabhq'),
+ import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
+ end
+
+ subject(:wiki) { described_class.new(project)}
+
+ describe '#path_with_namespace' do
+ it 'appends .wiki to project path' do
+ expect(wiki.path_with_namespace).to eq 'gitlabhq/gitlabhq.wiki'
+ end
+ end
+
+ describe '#import_url' do
+ it 'returns URL of the wiki repository' do
+ expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 154901a2fbc..8f86c491d3f 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -104,6 +104,14 @@ describe Notify do
it { is_expected.to have_body_text /View Commit/ }
end
+ shared_examples 'an unsubscribeable thread' do
+ it { is_expected.to have_body_text /unsubscribe/ }
+ end
+
+ shared_examples "a user cannot unsubscribe through footer link" do
+ it { is_expected.not_to have_body_text /unsubscribe/ }
+ end
+
describe 'for new users, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
@@ -115,6 +123,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'contains the password text' do
is_expected.to have_body_text /Click here to set your password/
@@ -134,7 +143,6 @@ describe Notify do
end
end
-
describe 'for users that signed up, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
@@ -144,6 +152,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'should not contain the new user\'s password' do
is_expected.not_to have_body_text /password/
@@ -157,6 +166,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do
is_expected.to deliver_to key.user.email
@@ -181,6 +191,7 @@ describe Notify do
subject { Notify.new_email_email(email.id) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do
is_expected.to deliver_to email.user.email
@@ -227,6 +238,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
@@ -253,6 +265,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -283,6 +296,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -319,6 +333,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -345,6 +360,7 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/
@@ -357,6 +373,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -387,6 +404,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -417,6 +435,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -446,6 +465,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Project was moved/
@@ -468,6 +488,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/
@@ -518,6 +539,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit'
it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
@@ -538,6 +560,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -558,6 +581,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
@@ -579,6 +603,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/
@@ -607,6 +632,7 @@ describe Notify do
subject { ActionMailer::Base.deliveries.last }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent to the new user' do
is_expected.to deliver_to 'new-email@mail.com'
@@ -629,6 +655,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', 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"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -657,6 +684,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', 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"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -684,6 +712,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', 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"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -707,6 +736,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', 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"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -734,6 +764,7 @@ describe Notify do
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) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -839,6 +870,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 1c22e3cb7c4..0e13456723d 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1,28 +1,3 @@
-# == Schema Information
-#
-# Table name: builds
-#
-# id :integer not null, primary key
-# project_id :integer
-# status :string(255)
-# finished_at :datetime
-# trace :text
-# created_at :datetime
-# updated_at :datetime
-# started_at :datetime
-# runner_id :integer
-# commit_id :integer
-# coverage :float
-# commands :text
-# job_id :integer
-# name :string(255)
-# deploy :boolean default(FALSE)
-# options :text
-# allow_failure :boolean default(FALSE), not null
-# stage :string(255)
-# trigger_request_id :integer
-#
-
require 'spec_helper'
describe Ci::Build, models: true do
@@ -368,21 +343,75 @@ describe Ci::Build, models: true do
end
end
- describe :download_url do
- subject { build.download_url }
+ describe :artifacts_download_url do
+ subject { build.artifacts_download_url }
it "should be nil if artifact doesn't exist" do
build.update_attributes(artifacts_file: nil)
is_expected.to be_nil
end
- it 'should be nil if artifact exist' do
+ it 'should not be nil if artifact exist' do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
build.update_attributes(artifacts_file: gif)
is_expected.to_not be_nil
end
end
+ describe :artifacts_browse_url do
+ subject { build.artifacts_browse_url }
+
+ it "should be nil if artifacts browser is unsupported" do
+ allow(build).to receive(:artifacts_browser_supported?).and_return(false)
+ is_expected.to be_nil
+ end
+
+ it 'should not be nil if artifacts browser is supported' do
+ allow(build).to receive(:artifacts_browser_supported?).and_return(true)
+ is_expected.to_not be_nil
+ end
+ end
+
+ describe :artifacts? do
+ subject { build.artifacts? }
+
+ context 'artifacts archive does not exist' do
+ before { build.update_attributes(artifacts_file: nil) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive exists' do
+ before do
+ gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ build.update_attributes(artifacts_file: gif)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+
+ describe :artifacts_browser_supported? do
+ subject { build.artifacts_browser_supported? }
+ context 'artifacts metadata does not exist' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive is a zip file and metadata exists' do
+ before do
+ fixture_dir = Rails.root + 'spec/fixtures/'
+ archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip',
+ 'application/zip')
+ metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz',
+ 'application/x-gzip')
+ build.update_attributes(artifacts_file: archive)
+ build.update_attributes(artifacts_metadata: metadata)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe :repo_url do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 876b927eaea..a2085df5bcd 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -36,6 +36,13 @@ describe ProjectWiki, models: true do
end
end
+ describe "#wiki_base_path" do
+ it "returns the wiki base path" do
+ wiki_base_path = "/#{project.path_with_namespace}/wikis"
+ expect(subject.wiki_base_path).to eq(wiki_base_path)
+ end
+ end
+
describe "#wiki" do
it "contains a Gollum::Wiki instance" do
expect(subject.wiki).to be_a Gollum::Wiki
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
new file mode 100644
index 00000000000..8c9f5a382b7
--- /dev/null
+++ b/spec/requests/api/builds_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) }
+ let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) }
+ let(:commit) { create(:ci_commit, project: project)}
+ let(:build) { create(:ci_build, commit: commit) }
+ let(:build_with_trace) { create(:ci_build_with_trace, commit: commit) }
+ let(:build_canceled) { create(:ci_build, :canceled, commit: commit) }
+
+ describe 'GET /projects/:id/builds ' do
+ context 'authorized user' do
+ it 'should return project builds' do
+ get api("/projects/#{project.id}/builds", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'should filter project with one scope element' do
+ get api("/projects/#{project.id}/builds?scope=pending", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'should filter project with array of scope elements' do
+ get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=running", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'should respond 400 when scope contains invalid state' do
+ get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=unknown_status", user)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project builds' do
+ get api("/projects/#{project.id}/builds")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/commits/:sha/builds' do
+ context 'authorized user' do
+ it 'should return project builds for specific commit' do
+ project.ensure_ci_commit(commit.sha)
+ get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project builds' do
+ project.ensure_ci_commit(commit.sha)
+ get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id' do
+ context 'authorized user' do
+ it 'should return specific build data' do
+ get api("/projects/#{project.id}/builds/#{build.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq('test')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return specific build data' do
+ get api("/projects/#{project.id}/builds/#{build.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id/trace' do
+ context 'authorized user' do
+ it 'should return specific build trace' do
+ get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace", user)
+
+ expect(response.status).to eq(200)
+ expect(response.body).to eq(build_with_trace.trace)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return specific build trace' do
+ get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/cancel' do
+ context 'authorized user' do
+ context 'user with :manage_builds persmission' do
+ it 'should cancel running or pending build' do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel", user)
+
+ expect(response.status).to eq(201)
+ expect(project.builds.first.status).to eq('canceled')
+ end
+ end
+
+ context 'user without :manage_builds permission' do
+ it 'should not cancel build' do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not cancel build' do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/retry' do
+ context 'authorized user' do
+ context 'user with :manage_builds persmission' do
+ it 'should retry non-running build' do
+ post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user)
+
+ expect(response.status).to eq(201)
+ expect(project.builds.first.status).to eq('canceled')
+ expect(json_response['status']).to eq('pending')
+ end
+ end
+
+ context 'user without :manage_builds permission' do
+ it 'should not retry build' do
+ post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not retry build' do
+ post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
index a28607bd240..21482fc1070 100644
--- a/spec/requests/api/commit_status_spec.rb
+++ b/spec/requests/api/commit_status_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::CommitStatus, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -12,6 +12,10 @@ describe API::API, api: true do
let(:commit_status) { create(:commit_status, commit: ci_commit) }
describe "GET /projects/:id/repository/commits/:sha/statuses" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) }
+ end
+
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index d8bbd107269..39f9a06fe1b 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -32,6 +32,10 @@ describe API::API, api: true do
before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
+ end
+
context "when noteable is an Issue" do
it "should return an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index c27e87c4acc..648ea0d5f50 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -210,6 +210,52 @@ describe Ci::API::API do
end
end
+ context 'should post artifacts file and metadata file' do
+ let!(:artifacts) { file_upload }
+ let!(:metadata) { file_upload2 }
+
+ let(:stored_artifacts_file) { build.reload.artifacts_file.file }
+ let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
+
+ before do
+ build.run!
+ post(post_url, post_data, headers_with_token)
+ end
+
+ context 'post data accelerated by workhorse is correct' do
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'metadata.path' => metadata.path,
+ 'metadata.name' => metadata.original_filename }
+ end
+
+ it 'responds with valid status' do
+ expect(response.status).to eq(201)
+ end
+
+ it 'stores artifacts and artifacts metadata' do
+ expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
+ expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
+ end
+ end
+
+ context 'no artifacts file in post data' do
+ let(:post_data) do
+ { 'metadata' => metadata }
+ end
+
+ it 'is expected to respond with bad request' do
+ expect(response.status).to eq(400)
+ end
+
+ it 'does not store metadata' do
+ expect(stored_metadata_file).to be_nil
+ end
+ end
+ end
+
+
context "should fail to post too large artifact" do
before do
build.run!
diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb
new file mode 100644
index 00000000000..352a6eeec79
--- /dev/null
+++ b/spec/support/api/pagination_shared_examples.rb
@@ -0,0 +1,20 @@
+# Specs for paginated resources.
+#
+# Requires an API request:
+# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
+shared_examples 'a paginated resources' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'has pagination headers' do
+ expect(response.headers).to include('X-Total')
+ expect(response.headers).to include('X-Total-Pages')
+ expect(response.headers).to include('X-Per-Page')
+ expect(response.headers).to include('X-Page')
+ expect(response.headers).to include('X-Next-Page')
+ expect(response.headers).to include('X-Prev-Page')
+ expect(response.headers).to include('Link')
+ end
+end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index 5d97fdd4882..73c6792b65f 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -28,6 +28,10 @@ class MarkdownFeature
end
end
+ def project_wiki
+ @project_wiki ||= ProjectWiki.new(project, user)
+ end
+
def issue
@issue ||= create(:issue, project: project)
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index b251e7f8f23..1d52489e804 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -66,6 +66,24 @@ module MarkdownMatchers
end
end
+ # GollumTagsFilter
+ matcher :parse_gollum_tags do
+ def have_image(src)
+ have_css("img[src$='#{src}']")
+ end
+
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_link('linked-resource', href: 'linked-resource')
+ expect(actual).to have_link('link-text', href: 'linked-resource')
+ expect(actual).to have_link('http://example.com', href: 'http://example.com')
+ expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf')
+ expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg')
+ expect(actual).to have_image('http://example.com/images/example.jpg')
+ end
+ end
+
# UserReferenceFilter
matcher :reference_users do
set_default_markdown_messages
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
new file mode 100755
index 00000000000..cfa49e72c50
--- /dev/null
+++ b/vendor/assets/javascripts/autosize.js
@@ -0,0 +1,243 @@
+/*!
+ Autosize 3.0.14
+ license: MIT
+ http://www.jacklmoore.com/autosize
+*/
+(function (global, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define(['exports', 'module'], factory);
+ } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
+ factory(exports, module);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod.exports, mod);
+ global.autosize = mod.exports;
+ }
+})(this, function (exports, module) {
+ 'use strict';
+
+ var set = typeof Set === 'function' ? new Set() : (function () {
+ var list = [];
+
+ return {
+ has: function has(key) {
+ return Boolean(list.indexOf(key) > -1);
+ },
+ add: function add(key) {
+ list.push(key);
+ },
+ 'delete': function _delete(key) {
+ list.splice(list.indexOf(key), 1);
+ } };
+ })();
+
+ function assign(ta) {
+ var _ref = arguments[1] === undefined ? {} : arguments[1];
+
+ var _ref$setOverflowX = _ref.setOverflowX;
+ var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
+ var _ref$setOverflowY = _ref.setOverflowY;
+ var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
+
+ if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
+
+ var heightOffset = null;
+ var overflowY = null;
+ var clientWidth = ta.clientWidth;
+
+ function init() {
+ var style = window.getComputedStyle(ta, null);
+
+ overflowY = style.overflowY;
+
+ if (style.resize === 'vertical') {
+ ta.style.resize = 'none';
+ } else if (style.resize === 'both') {
+ ta.style.resize = 'horizontal';
+ }
+
+ if (style.boxSizing === 'content-box') {
+ heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
+ } else {
+ heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
+ }
+ // Fix when a textarea is not on document body and heightOffset is Not a Number
+ if (isNaN(heightOffset)) {
+ heightOffset = 0;
+ }
+
+ update();
+ }
+
+ function changeOverflow(value) {
+ {
+ // Chrome/Safari-specific fix:
+ // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
+ // made available by removing the scrollbar. The following forces the necessary text reflow.
+ var width = ta.style.width;
+ ta.style.width = '0px';
+ // Force reflow:
+ /* jshint ignore:start */
+ ta.offsetWidth;
+ /* jshint ignore:end */
+ ta.style.width = width;
+ }
+
+ overflowY = value;
+
+ if (setOverflowY) {
+ ta.style.overflowY = value;
+ }
+
+ resize();
+ }
+
+ function resize() {
+ var htmlTop = window.pageYOffset;
+ var bodyTop = document.body.scrollTop;
+ var originalHeight = ta.style.height;
+
+ ta.style.height = 'auto';
+
+ var endHeight = ta.scrollHeight + heightOffset;
+
+ if (ta.scrollHeight === 0) {
+ // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
+ ta.style.height = originalHeight;
+ return;
+ }
+
+ ta.style.height = endHeight + 'px';
+
+ // used to check if an update is actually necessary on window.resize
+ clientWidth = ta.clientWidth;
+
+ // prevents scroll-position jumping
+ document.documentElement.scrollTop = htmlTop;
+ document.body.scrollTop = bodyTop;
+ }
+
+ function update() {
+ var startHeight = ta.style.height;
+
+ resize();
+
+ var style = window.getComputedStyle(ta, null);
+
+ if (style.height !== ta.style.height) {
+ if (overflowY !== 'visible') {
+ changeOverflow('visible');
+ }
+ } else {
+ if (overflowY !== 'hidden') {
+ changeOverflow('hidden');
+ }
+ }
+
+ if (startHeight !== ta.style.height) {
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:resized', true, false);
+ ta.dispatchEvent(evt);
+ }
+ }
+
+ var pageResize = function pageResize() {
+ if (ta.clientWidth !== clientWidth) {
+ update();
+ }
+ };
+
+ var destroy = (function (style) {
+ window.removeEventListener('resize', pageResize, false);
+ ta.removeEventListener('input', update, false);
+ ta.removeEventListener('keyup', update, false);
+ ta.removeEventListener('autosize:destroy', destroy, false);
+ ta.removeEventListener('autosize:update', update, false);
+ set['delete'](ta);
+
+ Object.keys(style).forEach(function (key) {
+ ta.style[key] = style[key];
+ });
+ }).bind(ta, {
+ height: ta.style.height,
+ resize: ta.style.resize,
+ overflowY: ta.style.overflowY,
+ overflowX: ta.style.overflowX,
+ wordWrap: ta.style.wordWrap });
+
+ ta.addEventListener('autosize:destroy', destroy, false);
+
+ // IE9 does not fire onpropertychange or oninput for deletions,
+ // so binding to onkeyup to catch most of those events.
+ // There is no way that I know of to detect something like 'cut' in IE9.
+ if ('onpropertychange' in ta && 'oninput' in ta) {
+ ta.addEventListener('keyup', update, false);
+ }
+
+ window.addEventListener('resize', pageResize, false);
+ ta.addEventListener('input', update, false);
+ ta.addEventListener('autosize:update', update, false);
+ set.add(ta);
+
+ if (setOverflowX) {
+ ta.style.overflowX = 'hidden';
+ ta.style.wordWrap = 'break-word';
+ }
+
+ init();
+ }
+
+ function destroy(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:destroy', true, false);
+ ta.dispatchEvent(evt);
+ }
+
+ function update(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:update', true, false);
+ ta.dispatchEvent(evt);
+ }
+
+ var autosize = null;
+
+ // Do nothing in Node.js environment and IE8 (or lower)
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
+ autosize = function (el) {
+ return el;
+ };
+ autosize.destroy = function (el) {
+ return el;
+ };
+ autosize.update = function (el) {
+ return el;
+ };
+ } else {
+ autosize = function (el, options) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], function (x) {
+ return assign(x, options);
+ });
+ }
+ return el;
+ };
+ autosize.destroy = function (el) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], destroy);
+ }
+ return el;
+ };
+ autosize.update = function (el) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], update);
+ }
+ return el;
+ };
+ }
+
+ module.exports = autosize;
+}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js
new file mode 100644
index 00000000000..da37966b28a
--- /dev/null
+++ b/vendor/assets/javascripts/latinise.js
@@ -0,0 +1,11 @@
+// Converting text to basic latin (aka removing accents)
+//
+// Based on: http://semplicewebsites.com/removing-accents-javascript
+//
+var Latinise = {
+ map: {"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"}
+};
+
+String.prototype.latinise = function() {
+ return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
+};