diff options
149 files changed, 2228 insertions, 370 deletions
diff --git a/CHANGELOG b/CHANGELOG index 3931f79c604..3995c3aa8e7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,8 @@ 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) @@ -17,8 +16,11 @@ v 8.4.0 (unreleased) - Fix missing date of month in network graph when commits span a month (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) + - Remove gray background from layout in UI + - Fix signup for OAuth providers that don't provide a name - Implement new UI for group page - Implement search inside emoji picker + - Let the CI runner know about builds that this build depends on - Add API support for looking up a user by username (Stan Hu) - Add project permissions to all project API endpoints (Stan Hu) - Link to milestone in "Milestone changed" system note @@ -48,8 +50,13 @@ v 8.4.0 (unreleased) - Allow subsequent validations in CI Linter - Show referenced MRs & Issues only when the current viewer can access them - Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee) - - Add API support for managing build variables of project + - Add API support for managing project's builds + - Add API support for managing project's build triggers + - Add API support for managing project's build variables - Allow broadcast messages to be edited + - Autosize Markdown textareas + - Import GitHub wiki into GitLab + - Add reporters ability to download and browse build artifacts (Andrew Johnson) v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) @@ -2,6 +2,6 @@ # https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in # lib/support/init.d, which call scripts in bin/ . # -web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default +web: RAILS_ENV=development bin/web start_foreground +worker: RAILS_ENV=development bin/background_jobs start_foreground # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..c90d078406c --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..b87e22c41b5 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..0f46f3e833a --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..8007df6df32 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..b715f274082 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..d8f9d29d4aa --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..a00852641f8 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-It.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..d8b610ad76e --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..e0eebac8273 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..0dd3464c74b --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..2526d2e1b60 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 Binary files differnew file mode 100755 index 00000000000..606935af089 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 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/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index fcf50dd1b51..8866d81c925 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() @@ -353,7 +355,7 @@ class @Notes $('.note[id="' + note_id + '"]').each -> note = $(this) notes = note.closest(".notes") - count = notes.closest(".notes_holder").find(".discussion-notes-count") + count = notes.closest(".issuable-details").find(".notes-tab .badge") # check if this is the last note for this line if notes.find(".note").length is 1 @@ -363,9 +365,10 @@ class @Notes # for diff lines notes.closest("tr").remove() - else - # update notes count - count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}" + + # update notes count + oldNum = parseInt(count.text()) + count.text(oldNum - 1) note.remove() 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/fonts.scss b/app/assets/stylesheets/framework/fonts.scss index 20988f7b430..7a946109e3a 100644 --- a/app/assets/stylesheets/framework/fonts.scss +++ b/app/assets/stylesheets/framework/fonts.scss @@ -3,23 +3,39 @@ font-family: 'Source Sans Pro'; font-style: normal; font-weight: 300; - src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf.woff'); + src: + local('Source Sans Pro Light'), + local('SourceSansPro-Light'), + font-url('SourceSansPro-Light.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Light.ttf.woff') format('woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 400; - src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf.woff'); + src: + local('Source Sans Pro'), + local('SourceSansPro-Regular'), + font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Regular.ttf.woff') format('woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 600; - src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf.woff'); + src: + local('Source Sans Pro Semibold'), + local('SourceSansPro-Semibold'), + font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Semibold.ttf.woff') format('woff'); } @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 700; - src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf.woff'); + src: + local('Source Sans Pro Bold'), + local('SourceSansPro-Bold'), + font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'), + font-url('SourceSansPro-Bold.ttf.woff') format('woff'); } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index c540e527bed..6732343802a 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -83,6 +83,7 @@ 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/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 28c294714ad..0997dfc287c 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -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/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 55e834947d1..eae3590a189 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -105,8 +105,13 @@ text-overflow: ellipsis; } + cite { + font-style: normal; + } + button { float: right; + padding: 3px 5px; } } 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 4fabd765537..13b0ed769fc 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -53,6 +53,8 @@ } .notifications-btn { + margin-top: -28px; + .fa-bell { margin-right: 6px; } @@ -77,21 +79,6 @@ } } - .git-clone-holder { - max-width: 498px; - - .form-control { - background: #FFF; - font-size: 14px; - margin-left: -1px; - } - - .btn-clipboard { - border: 1px solid $border-color; - padding: 6px $gl-padding; - } - } - .visibility-level-label { @extend .btn; @extend .btn-gray; @@ -104,11 +91,6 @@ } } - .git-clone-holder { - display: inline-table; - position: relative; - } - .project-repo-buttons { margin-top: 12px; margin-bottom: 0px; @@ -118,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; @@ -195,74 +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; - } - } - } -} - -.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); @@ -603,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/controllers/application_controller.rb b/app/controllers/application_controller.rb index 81cb1367e2c..bf99b2e777d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -115,7 +115,7 @@ class ApplicationController < ActionController::Base # localhost/group/project # if id =~ /\.git\Z/ - redirect_to request.original_url.gsub(/\.git\Z/, '') and return + redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return end project_path = "#{namespace}/#{id}" diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index dff0732bdfe..f159a6d6dc6 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -8,7 +8,7 @@ class Projects::ArtifactsController < Projects::ApplicationController end unless artifacts_file.exists? - return not_found! + return render_404 end send_file artifacts_file.path, disposition: 'attachment' diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 0e965966ffa..92d9699fe84 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -42,7 +42,7 @@ class Projects::BuildsController < Projects::ApplicationController def retry unless @build.retryable? - return page_404 + return render_404 end build = Ci::Build.retry(@build) @@ -72,7 +72,7 @@ class Projects::BuildsController < Projects::ApplicationController def authorize_manage_builds! unless can?(current_user, :manage_builds, project) - return page_404 + return render_404 end end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 0aaba3792bf..870f6795219 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -79,7 +79,7 @@ class Projects::CommitController < Projects::ApplicationController def authorize_manage_builds! unless can?(current_user, :manage_builds, project) - return page_404 + return render_404 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/models/ability.rb b/app/models/ability.rb index 5375148a654..ab59a3506a2 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -160,6 +160,7 @@ class Ability @project_report_rules ||= project_guest_rules + [ :create_commit_status, :read_commit_statuses, + :read_build_artifacts, :download_code, :fork_project, :create_project_snippet, @@ -175,7 +176,6 @@ class Ability :create_merge_request, :create_wiki, :manage_builds, - :read_build_artifacts, :push_code ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6cc26abce66..16a5b03f591 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -128,6 +128,14 @@ module Ci !self.commit.latest_builds_for_ref(self.ref).include?(self) end + def depends_on_builds + # Get builds of the same type + latest_builds = self.commit.builds.similar(self).latest + + # Return builds from previous stages + latest_builds.where('stage_idx < ?', stage_idx) + end + def trace_html html = Ci::Ansi2html::convert(trace) if trace.present? html || '' diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index bb98cd5c7da..2b9a457c8ab 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -33,6 +33,10 @@ module Ci trigger_requests.last end + def last_used + last_trigger_request.try(:created_at) + end + def short_token token[0...10] end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4ce9b998dfc..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 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/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/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 1b0ae6c0056..1cd93263c9f 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -32,6 +32,10 @@ class ArtifactUploader < CarrierWave::Uploader::Base self.class.storage == CarrierWave::Storage::File end + def filename + file.try(:filename) + end + def exists? file.try(:exists?) end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 5e39d3ab601..ebb3df7dca3 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,5 +1,4 @@ - @no_container = true -- @blank_container = true - unless can?(current_user, :read_group, @group) - @disable_search_panel = true 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/_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/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/builds.html.haml b/app/views/projects/commit/builds.html.haml index 99d62503a94..7118a4846c6 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,6 +1,7 @@ - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" = render "projects/commits/header_title" -= render "commit_box" +.prepend-top-default + = render "commit_box" = render "ci_menu" = render "builds" diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 503d156661e..b34d106d565 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,3 +1,5 @@ +- @no_container = true + = content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' @@ -17,40 +19,41 @@ file to this project. - if can?(current_user, :download_code, @project) - .prepend-top-20 - .empty_wrapper - %h3.page-title-empty - Command line instructions - %div.git-empty - %fieldset - %h5 Git global setup - %pre.light-well - :preserve - git config --global user.name "#{h git_user_name}" - git config --global user.email "#{h git_user_email}" + %div{ class: container_class } + .prepend-top-20 + .empty_wrapper + %h3.page-title-empty + Command line instructions + %div.git-empty + %fieldset + %h5 Git global setup + %pre.light-well + :preserve + git config --global user.name "#{h git_user_name}" + git config --global user.email "#{h git_user_email}" - %fieldset - %h5 Create a new repository - %pre.light-well - :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} - cd #{h @project.path} - touch README.md - git add README.md - git commit -m "add README" - git push -u origin master + %fieldset + %h5 Create a new repository + %pre.light-well + :preserve + git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + cd #{h @project.path} + touch README.md + git add README.md + git commit -m "add README" + git push -u origin master - %fieldset - %h5 Existing folder or Git repository - %pre.light-well - :preserve - cd existing_folder - git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} - git add . - git commit - git push -u origin master + %fieldset + %h5 Existing folder or Git repository + %pre.light-well + :preserve + cd existing_folder + git init + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit + git push -u origin master - - if can? current_user, :remove_project, @project - .prepend-top-20 - = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + - if can? current_user, :remove_project, @project + .prepend-top-20 + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 797bee708bd..4310f038fc9 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,5 +1,4 @@ - @no_container = true -- @blank_container = true = content_for :meta_tags do - if current_user 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/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/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/users/show.html.haml b/app/views/users/show.html.haml index 284a62e6f47..849304ee2b6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -2,7 +2,6 @@ - page_description @user.bio - header_title @user.name, user_path(@user) - @no_container = true -- @blank_container = true = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") diff --git a/bin/background_jobs b/bin/background_jobs index 5c85fb339e6..1f67d732949 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -27,17 +27,17 @@ restart() stop fi killall - start_sidekiq -d -L $sidekiq_logfile + start_sidekiq -d -L $sidekiq_logfile >> $sidekiq_logfile 2>&1 } start_no_deamonize() { - start_sidekiq + start_sidekiq >> $sidekiq_logfile 2>&1 } start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@" } load_ok() @@ -66,6 +66,9 @@ case "$1" in start_no_deamonize) start_no_deamonize ;; + start_foreground) + start_sidekiq + ;; restart) restart ;; @@ -5,6 +5,7 @@ app_root=$(pwd) unicorn_pidfile="$app_root/tmp/pids/unicorn.pid" unicorn_config="$app_root/config/unicorn.rb" +unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV" get_unicorn_pid() { @@ -18,7 +19,12 @@ get_unicorn_pid() start() { - bundle exec unicorn_rails -D -c $unicorn_config -E $RAILS_ENV + $unicorn_cmd -D +} + +start_foreground() +{ + $unicorn_cmd } stop() @@ -37,6 +43,9 @@ case "$1" in start) start ;; + start_foreground) + start_foreground + ;; stop) stop ;; diff --git a/config/environments/development.rb b/config/environments/development.rb index c22722c606b..257c163720a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -34,6 +34,8 @@ Rails.application.configure do config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Open sent mails in browser config.action_mailer.delivery_method = :letter_opener + # Don't make a mess when bootstrapping a development environment + config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') config.eager_load = false end diff --git a/config/routes.rb b/config/routes.rb index b6cdac63e81..75418db8d25 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -519,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 diff --git a/doc/api/README.md b/doc/api/README.md index c3401bcbc9d..2fa177ff4dd 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -23,6 +23,8 @@ - [Namespaces](namespaces.md) - [Settings](settings.md) - [Keys](keys.md) +- [Builds](builds.md) +- [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) ## Clients diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md new file mode 100644 index 00000000000..4a12e962b62 --- /dev/null +++ b/doc/api/build_triggers.md @@ -0,0 +1,108 @@ +# Build triggers + +You can read more about [triggering builds through the API](../ci/triggers/README.md). + +## List project triggers + +Get a list of project's build triggers. + +``` +GET /projects/:id/triggers +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +``` + +```json +[ + { + "created_at": "2015-12-23T16:24:34.716Z", + "deleted_at": null, + "last_used": "2016-01-04T15:41:21.986Z", + "token": "fbdb730c2fbdb095a0862dbd8ab88b", + "updated_at": "2015-12-23T16:24:34.716Z" + }, + { + "created_at": "2015-12-23T16:25:56.760Z", + "deleted_at": null, + "last_used": null, + "token": "7b9148c158980bbd9bcea92c17522d", + "updated_at": "2015-12-23T16:25:56.760Z" + } +] +``` + +## Get trigger details + +Get details of project's build trigger. + +``` +GET /projects/:id/triggers/:token +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `token` | string | yes | The `token` of a trigger | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +``` + +```json +{ + "created_at": "2015-12-23T16:25:56.760Z", + "deleted_at": null, + "last_used": null, + "token": "7b9148c158980bbd9bcea92c17522d", + "updated_at": "2015-12-23T16:25:56.760Z" +} +``` + +## Create a project trigger + +Create a build trigger for a project. + +``` +POST /projects/:id/triggers +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +``` + +```json +{ + "created_at": "2016-01-07T09:53:58.235Z", + "deleted_at": null, + "last_used": null, + "token": "6d056f63e50fe6f8c5f8f4aa10edb7", + "updated_at": "2016-01-07T09:53:58.235Z" +} +``` + +## Remove a project trigger + +Remove a project's build trigger. + +``` +DELETE /projects/:id/triggers/:token +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|--------------------------| +| `id` | integer | yes | The ID of a project | +| `token` | string | yes | The `token` of a project | + +``` +curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +``` 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/ci/api/builds.md b/doc/ci/api/builds.md index 3b5008ccdb4..018ca22dbbd 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -18,18 +18,62 @@ Returns: ```json { - "id" : 79, - "commands" : "", - "path" : "", - "ref" : "", - "sha" : "", - "project_id" : 6, - "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "before_sha" : "" + "id": 48584, + "ref": "0.1.1", + "tag": true, + "sha": "d63117656af6ff57d99e50cc270f854691f335ad", + "status": "success", + "name": "pages", + "token": "9dd60b4f1a439d1765357446c1084c", + "stage": "test", + "project_id": 479, + "project_name": "test", + "commands": "echo commands", + "repo_url": "http://gitlab-ci-token:token@gitlab.example/group/test.git", + "before_sha": "0000000000000000000000000000000000000000", + "allow_git_fetch": false, + "options": { + "image": "docker:image", + "artifacts": { + "paths": [ + "public" + ] + }, + "cache": { + "paths": [ + "vendor" + ] + } + }, + "timeout": 3600, + "variables": [ + { + "key": "CI_BUILD_TAG", + "value": "0.1.1", + "public": true + } + ], + "depends_on_builds": [ + { + "id": 48584, + "ref": "0.1.1", + "tag": true, + "sha": "d63117656af6ff57d99e50cc270f854691f335ad", + "status": "success", + "name": "build", + "token": "9dd60b4f1a439d1765357446c1084c", + "stage": "build", + "project_id": 479, + "project_name": "test", + "artifacts_file": { + "filename": "artifacts.zip", + "size": 0 + } + } + ] } ``` - ### Update details of an existing build PUT /ci/builds/:id 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/doc/workflow/importing/github_importer/importer.png b/doc/workflow/importing/github_importer/importer.png Binary files differdeleted file mode 100644 index 57636717571..00000000000 --- a/doc/workflow/importing/github_importer/importer.png +++ /dev/null diff --git a/doc/workflow/importing/github_importer/new_project_page.png b/doc/workflow/importing/github_importer/new_project_page.png Binary files differdeleted file mode 100644 index 002f22d81d7..00000000000 --- a/doc/workflow/importing/github_importer/new_project_page.png +++ /dev/null diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png Binary files differnew file mode 100644 index 00000000000..f744dc06f81 --- /dev/null +++ b/doc/workflow/importing/img/import_projects_from_github_importer.png diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png Binary files differnew file mode 100644 index 00000000000..86be35acb37 --- /dev/null +++ b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 2027a055c37..77fb7ea7cd6 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -1,20 +1,46 @@ # Import your project from GitHub to GitLab
-It takes just a couple of steps to import your existing GitHub projects to GitLab. Keep in mind that it is possible only if
-GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html)
+_**Note:** In order to enable the GitHub import setting, you should first
+enable the [GitHub integration][gh-import] in your GitLab instance._
-If you want to import from a GitHub Enterprise instance, you need to use GitLab Enterprise; please see the [EE docs for the GitHub integration](http://doc.gitlab.com/ee/integration/github.html).
+At its current state, GitHub importer can import:
-* Sign in to GitLab.com and go to your dashboard.
-* To get to the importer page, you need to go to the "New project" page.
+- the repository description
+- the git repository data
+- the issues
+- the pull requests
+- the wiki pages
-![New project page](github_importer/new_project_page.png)
+The importer page is visible when you [create a new project][new-project].
+Click on the **GitHub** link and you will be redirected to GitHub for
+permission to access your projects. After accepting, you'll be automatically
+redirected to the importer.
-* Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
+![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
-![Importer page](github_importer/importer.png)
+---
-* To import a project, you can simple click "Add". The importer will import your repository, issues, and pull requests. Once the importer is done, a new GitLab project will be created with your imported data.
+While at the GitHub importer page, you can see the import statuses of your
+GitHub projects. Those that are being imported will show a _started_ status,
+those already imported will be green, whereas those that are not yet imported
+have an **Import** button on the right side of the table. If you want, you can
+import all your GitHub projects in one go by hitting **Import all projects**
+in the upper left corner.
+
+![GitHub importer page](img/import_projects_from_github_importer.png)
+
+---
+
+The importer will create any new namespaces if they don't exist or in the
+case the namespace is taken, the project will be imported on the user's
+namespace.
### Note
-When you import your projects from GitHub, it is not possible to keep your labels, milestones, and cross-repository pull requests. We are working on improving this in the near future.
+
+When you import your projects from GitHub, it is not possible to keep your
+labels, milestones, and cross-repository pull requests. We are working on
+improving this in the near future.
+
+[gh-import]: ../../integration/github.md "GitHub integration"
+[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
+[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
diff --git a/features/project/builds.feature b/features/project/builds/artifacts.feature index c00b0a7ae07..7a7dbb71b18 100644 --- a/features/project/builds.feature +++ b/features/project/builds/artifacts.feature @@ -1,14 +1,9 @@ -Feature: Project Builds +Feature: Project Builds Artifacts 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 + And project has CI enabled + And project has a recent build Scenario: I download build artifacts Given recent build has artifacts available diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature new file mode 100644 index 00000000000..1193bcd74f6 --- /dev/null +++ b/features/project/builds/permissions.feature @@ -0,0 +1,18 @@ +Feature: Project Builds Permissions + Background: + Given I sign in as a user + And project exists in some group namespace + And project has CI enabled + And project has a recent build + + Scenario: I try to download build artifacts as guest + Given I am member of a project with a guest role + And recent build has artifacts available + When I access artifacts download page + Then page status code should be 404 + + Scenario: I try to download build artifacts as reporter + Given I am member of a project with a reporter role + And recent build has artifacts available + When I access artifacts download page + Then download of build artifacts archive starts diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature new file mode 100644 index 00000000000..e90ea592aab --- /dev/null +++ b/features/project/builds/summary.feature @@ -0,0 +1,11 @@ +Feature: Project Builds Summary + Background: + Given I sign in as a user + And I own a project + And project has CI enabled + And project has a recent build + + Scenario: I browse build summary page + When I visit recent build summary page + Then I see summary for build + And I see build trace 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/artifacts.rb index 28395281077..f2c87da4717 100644 --- a/features/steps/project/builds.rb +++ b/features/steps/project/builds/artifacts.rb @@ -1,26 +1,13 @@ -class Spinach::Features::ProjectBuilds < Spinach::FeatureSteps +class Spinach::Features::ProjectBuildsArtifacts < 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 diff --git a/features/steps/project/builds/permissions.rb b/features/steps/project/builds/permissions.rb new file mode 100644 index 00000000000..6e9d6504fd5 --- /dev/null +++ b/features/steps/project/builds/permissions.rb @@ -0,0 +1,7 @@ +class Spinach::Features::ProjectBuildsPermissions < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include SharedPaths + include RepoHelpers +end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb new file mode 100644 index 00000000000..2439d48fbef --- /dev/null +++ b/features/steps/project/builds/summary.rb @@ -0,0 +1,14 @@ +class Spinach::Features::ProjectBuildsSummary < 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 +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/builds.rb b/features/steps/shared/builds.rb index a83d74e5946..f88b01af84e 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -1,11 +1,11 @@ module SharedBuilds include Spinach::DSL - step 'CI is enabled' do + step 'project has CI enabled' do @project.enable_ci end - step 'I have recent build for my project' do + step 'project has a recent build' do ci_commit = create :ci_commit, project: @project, sha: sample_commit.id @build = create :ci_build, commit: ci_commit end @@ -25,4 +25,13 @@ module SharedBuilds gzip = fixture_file_upload(metadata, 'application/x-gzip') @build.update_attributes(artifacts_metadata: gzip) 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 access artifacts download page' do + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build) + end end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index d3501b5f5cb..d9c75d12238 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -7,6 +7,11 @@ module SharedProject @project.team << [@user, :master] end + step "project exists in some group namespace" do + @group = create(:group, name: 'some group') + @project = create(:project, namespace: @group) + end + # Create a specific project called "Shop" step 'I own project "Shop"' do @project = Project.find_by(name: "Shop") @@ -98,6 +103,18 @@ module SharedProject end # ---------------------------------------- + # Project permissions + # ---------------------------------------- + + step 'I am member of a project with a guest role' do + @project.team << [@user, Gitlab::Access::GUEST] + end + + step 'I am member of a project with a reporter role' do + @project.team << [@user, Gitlab::Access::REPORTER] + end + + # ---------------------------------------- # Visibility of archived project # ---------------------------------------- @@ -229,5 +246,4 @@ module SharedProject project ||= create(:empty_project, visibility, name: project_name, namespace: user.namespace) project.team << [user, :master] 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 e2feb7bb6d9..82a75734de0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -367,6 +367,37 @@ 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 + class Variable < Grape::Entity expose :key, :value end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index d46b5c42967..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! @@ -329,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/api/triggers.rb b/lib/api/triggers.rb index 2781f1cf191..5e4964f446c 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -43,6 +43,75 @@ module API render_api_error!(errors, 400) end end + + # Get triggers list + # + # Parameters: + # id (required) - The ID of a project + # page (optional) - The page number for pagination + # per_page (optional) - The value of items per page to show + # Example Request: + # GET /projects/:id/triggers + get ':id/triggers' do + authenticate! + authorize_admin_project + + triggers = user_project.triggers.includes(:trigger_requests) + triggers = paginate(triggers) + + present triggers, with: Entities::Trigger + end + + # Get specific trigger of a project + # + # Parameters: + # id (required) - The ID of a project + # token (required) - The `token` of a trigger + # Example Request: + # GET /projects/:id/triggers/:token + get ':id/triggers/:token' do + authenticate! + authorize_admin_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + present trigger, with: Entities::Trigger + end + + # Create trigger + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # POST /projects/:id/triggers + post ':id/triggers' do + authenticate! + authorize_admin_project + + trigger = user_project.triggers.create + + present trigger, with: Entities::Trigger + end + + # Delete trigger + # + # Parameters: + # id (required) - The ID of a project + # token (required) - The `token` of a trigger + # Example Request: + # DELETE /projects/:id/triggers/:token + delete ':id/triggers/:token' do + authenticate! + authorize_admin_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + trigger.destroy + + present trigger, with: Entities::Trigger + end end end end 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 fb87637b94f..690bbf97a89 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -20,7 +20,7 @@ module Ci if build update_runner_info - present build, with: Entities::Build + present build, with: Entities::BuildDetails else not_found! end @@ -111,7 +111,7 @@ module Ci build.artifacts_metadata = metadata if build.save - present(build, with: Entities::Build) + present(build, with: Entities::BuildDetails) else render_validation_error!(build) end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index e4ac0545ea2..b25e0e573a8 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -16,10 +16,19 @@ module Ci end class Build < Grape::Entity - expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url, - :before_sha, :allow_git_fetch, :project_name - + expose :id, :ref, :tag, :sha, :status expose :name, :token, :stage + expose :project_id + expose :project_name + expose :artifacts_file, using: ArtifactFile, if: lambda { |build, opts| build.artifacts? } + end + + class BuildDetails < Build + expose :commands + expose :repo_url + expose :before_sha + expose :allow_git_fetch + expose :token expose :options do |model| model.options @@ -30,7 +39,7 @@ module Ci end expose :variables - expose :artifacts_file, using: ArtifactFile + expose :depends_on_builds, using: Build end class Runner < Grape::Entity 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/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index f1a362f5303..e3d2cc65a8f 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -141,9 +141,12 @@ module Gitlab username = auth_hash.username email = auth_hash.email end + + name = auth_hash.name + name = ::Namespace.clean_path(username) if name.strip.empty? { - name: auth_hash.name, + name: name, username: ::Namespace.clean_path(username), email: email, password: auth_hash.password, 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/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/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index db053c610cd..2c0d004d267 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -3,6 +3,8 @@ FactoryGirl.define do factory :ci_trigger_request, class: Ci::TriggerRequest do factory :ci_trigger_request_with_variables do + trigger factory: :ci_trigger + variables do { TRIGGER_KEY: 'TRIGGER_VALUE' 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/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/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/models/build_spec.rb b/spec/models/build_spec.rb index 0e13456723d..d12b9e65c82 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -426,6 +426,30 @@ describe Ci::Build, models: true do it { is_expected.to include(project.web_url[7..-1]) } end + describe :depends_on_builds do + let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' } + let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' } + let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' } + let!(:staging) { FactoryGirl.create :ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy' } + + it 'to have no dependents if this is first build' do + expect(build.depends_on_builds).to be_empty + end + + it 'to have one dependent if this is test' do + expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) + end + + it 'to have all builds from build and test stage if this is last' do + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) + end + + it 'to have retried builds instead the original ones' do + retried_rspec = Ci::Build.retry(rspec_test) + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + end + end + def create_mr(build, commit, factory: :merge_request, created_at: Time.now) FactoryGirl.create(factory, source_project_id: commit.gl_project_id, 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/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 314bd7ddc59..2a86b60bc4d 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -3,11 +3,19 @@ require 'spec_helper' describe API::API do include ApiHelpers + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } + let!(:trigger_token_2) { 'secure_token_2' } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } + let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } + let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') } + describe 'POST /projects/:project_id/trigger' do - let!(:trigger_token) { 'secure token' } - let!(:project) { FactoryGirl.create(:project) } - let!(:project2) { FactoryGirl.create(:empty_project) } - let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } + let!(:project2) { create(:empty_project) } let(:options) do { token: trigger_token @@ -77,4 +85,127 @@ describe API::API do end end end + + describe 'GET /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'should return list of triggers' do + get api("/projects/#{project.id}/triggers", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Array) + expect(json_response[0]).to have_key('token') + end + end + + context 'authenticated user with invalid permissions' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'should return trigger details' do + get api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Hash) + end + + it 'should respond with 404 Not Found if requesting non-existing trigger' do + get api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response.status).to eq(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'should create trigger' do + expect do + post api("/projects/#{project.id}/triggers", user) + end.to change{project.triggers.count}.by(1) + + expect(response.status).to eq(201) + expect(json_response).to be_a(Hash) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not create trigger' do + post api("/projects/#{project.id}/triggers", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not create trigger' do + post api("/projects/#{project.id}/triggers") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'should delete trigger' do + expect do + delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + end.to change{project.triggers.count}.by(-1) + expect(response.status).to eq(200) + end + + it 'should respond with 404 Not Found if requesting non-existing trigger' do + delete api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response.status).to eq(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not delete trigger' do + delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not delete trigger' do + delete api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response.status).to eq(401) + end + end + end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 648ea0d5f50..eec927102aa 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -101,6 +101,18 @@ describe Ci::API::API do { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false }, ]) end + + it "returns dependent builds" do + commit = FactoryGirl.create(:ci_commit, project: project) + commit.create_builds('master', false, nil, nil) + commit.builds.where(stage: 'test').each(&:success) + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response["depends_on_builds"].count).to eq(2) + expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec") + end end describe "PUT /builds/:id" do 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/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index 3482145404e..a5b256bd3ec 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -36,8 +36,8 @@ staging: script: "cap deploy stating" type: deploy tags: - - capistrano - - debian + - ruby + - mysql except: - stable @@ -47,8 +47,8 @@ production: - cap deploy production - cap notify tags: - - capistrano - - debian + - ruby + - mysql only: - master - /^deploy-.*$/ 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; }); +}; |