diff options
84 files changed, 1718 insertions, 196 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index fe686b9fe8d..1893d5626df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,58 +4,6 @@ entry. ## 8.14.0 (2016-11-22) -- Centralize LDAP config/filter logic. !6606 -- Show random messages when the To Do list is empty. !6818 (Josep Llaneras) -- Fix record not found error on NewNoteWorker processing. !6863 (Oswaldo Ferreira) -- Fix expanding a collapsed diff when converting a symlink to a regular file. !6953 -- Add link to build pipeline within individual build pages. !7082 -- Add api endpoint `/groups/owned`. !7103 (Borja Aparicio) -- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry) -- Added ability to throttle Sidekiq Jobs. !7292 -- Require projects before creating milestone. !7301 (gfyoung) -- Fix error when using invalid branch name when creating a new pipeline. !7324 -- Fix cache for commit status in commits list to respect branches. !7372 -- Removed gray button styling from todo buttons in sidebars. !7387 -- Fix project records with invalid visibility_level values. !7391 -- Use 'Forking in progress' title when appropriate. !7394 (Philip Karpiak) -- Fix error links in help index page. !7396 (Fu Xu) -- [Fix] Extra divider issue in dropdown. !7398 -- Project download buttons always show. !7405 (Philip Karpiak) -- Give search-input correct padding-right value. !7407 (Philip Karpiak) -- Remove additional padding on right-aligned items in MR widget. !7411 (Didem Acet) -- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford) -- Fix project Visibility Level selector not using default values. -- Use separate email-token for incoming email and revert back the inactive feature. !5914 -- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps) -- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342 -- Finer-grained Git gargage collection. !6588 -- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601 -- Process commits using a dedicated Sidekiq worker. !6802 -- Fix showing pipeline status for a given commit from correct branch. !7034 -- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta) -- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps) -- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda) -- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato) -- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato) -- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger) -- Only skip group when it's actually a group in the "Share with group" select. !7262 -- Introduce round-robin project creation to spread load over multiple shards. !7266 -- Ensure merge request's "remove branch" accessors return booleans. !7267 -- Expose label IDs in API. !7275 (Rares Sfirlogea) -- Fix invalid filename validation on eslint. !7281 -- API: Ability to retrieve version information. !7286 (Robert Schilling) -- Set default Sidekiq retries to 3. !7294 -- Return 400 when creating a system hook fails. !7350 (Robert Schilling) -- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright) -- Add an index for project_id in project_import_data to improve performance. -- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose) -- Faster search inside Project. -- Clicking "force remove source branch" label now toggles the checkbox again. -- Allow to test JIRA service settings without having a repository. -- Fix: Guest sees some repository details and gets 404. -- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2. -- Fix: Todos Filter Shows All Users. -- Fix broken commits search. - Show correct environment log in admin/logs (@duk3luk3 !7191) - Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117 - Diff collapse won't shift when collapsing. diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 5c047dd4481..8d8431c424e 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -53,6 +53,7 @@ /*= require_directory ./u2f */ /*= require_directory . */ /*= require fuzzaldrin-plus */ +/*= require es6-promise.auto */ (function () { document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index e72e2194be8..19bfdf1de8c 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -16,7 +16,7 @@ }, // Team Members Members: { - template: '<li>${username} <small>${title}</small></li>' + template: '<li>${avatarTag} ${username} <small>${title}</small></li>' }, Labels: { template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' @@ -51,6 +51,11 @@ if (!GitLab.GfmAutoComplete.dataLoaded) { return this.at; } else { + if (value.indexOf("unlabel") !== -1) { + GitLab.GfmAutoComplete.input.atwho('load', '~', GitLab.GfmAutoComplete.cachedData.unlabels); + } else { + GitLab.GfmAutoComplete.input.atwho('load', '~', GitLab.GfmAutoComplete.cachedData.labels); + } return value; } } @@ -118,7 +123,7 @@ beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(members) { return $.map(members, function(m) { - var title; + let title = ''; if (m.username == null) { return m; } @@ -126,8 +131,14 @@ if (m.count) { title += " (" + m.count + ")"; } + + const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); + const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; + const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; + return { username: m.username, + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, title: gl.utils.sanitize(title), search: gl.utils.sanitize(m.username + " " + m.name) }; @@ -352,3 +363,4 @@ }; }).call(this); + diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 202ed5ae8fe..ad0d387067f 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -34,6 +34,7 @@ &.avatar-inline { float: none; + display: inline-block; margin-left: 4px; margin-bottom: 2px; @@ -41,6 +42,12 @@ &.s24 { margin-right: 4px; } } + &.center { + font-size: 14px; + line-height: 1.8em; + text-align: center; + } + &.avatar-tile { border-radius: 0; border: none; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0e0673a72f6..16ecf466931 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -63,7 +63,11 @@ header { &:focus, &:active { background-color: $background-color; - color: darken($gl-icon-color, 50%); + color: darken($gl-icon-color, 30%); + + .todos-pending-count { + background: darken($todo-alert-blue, 10%); + } } .fa-caret-down { @@ -194,7 +198,7 @@ header { cursor: pointer; &:hover { - color: darken($color: $gl-text-color, $amount: 50%); + color: darken($color: $gl-text-color, $amount: 30%); } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 6d28d98b283..8a93eac1b6d 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -148,7 +148,19 @@ } } -.atwho-view small.description { - float: right; - padding: 3px 5px; -} +.atwho-view { + small.description { + float: right; + padding: 3px 5px; + } + + .avatar-inline { + margin-bottom: 0; + } + + .cur { + .avatar { + border: 1px solid $white-light; + } + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 1a116f6a919..e9ba2e5c098 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -157,7 +157,6 @@ width: 68%; } } - } .search-holder { @@ -234,5 +233,4 @@ &:focus { color: $gl-link-color; } - } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a8a18b4fa16..7376c2bfeb7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -144,13 +144,15 @@ class ProjectsController < Projects::ApplicationController autocomplete = ::Projects::AutocompleteService.new(@project, current_user) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) + unlabels = autocomplete.unlabels(noteable) @suggestions = { emojis: Gitlab::AwardEmoji.urls, issues: autocomplete.issues, milestones: autocomplete.milestones, mergerequests: autocomplete.merge_requests, - labels: autocomplete.labels, + labels: autocomplete.labels - unlabels, + unlabels: unlabels, members: participants, commands: autocomplete.commands(noteable, params[:type]) } diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8127c3f3ee3..8b97cf07fb9 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -30,11 +30,6 @@ module IssuablesHelper end end - def can_add_template?(issuable) - names = issuable_templates(issuable) - names.empty? && can?(current_user, :push_code, @project) && !@project.private? - end - def template_dropdown_tag(issuable, &block) title = selected_template(issuable) || "Choose a template" options = { diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index 8cad994d10f..c41181bab3d 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,5 +1,9 @@ module TriggersHelper - def builds_trigger_url(project_id) - "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" + def builds_trigger_url(project_id, ref: nil) + if ref.nil? + "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" + else + "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds" + end end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 015f2828921..223461e88b6 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -13,7 +13,14 @@ module Projects end def labels - LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color]) + LabelsFinder.new(current_user, project_id: project.id).execute. + pluck(:title, :color).map { |l| { title: l.first, color: l.second } } + end + + def unlabels(noteable) + return [] unless noteable && noteable.respond_to?(:labels) + + noteable.labels.pluck(:title, :color).map { |l| { title: l.first, color: l.second } } end def commands(noteable, type) diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index d38328403c1..6040391fd94 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -1,7 +1,7 @@ module Projects class ParticipantsService < BaseService attr_reader :noteable - + def execute(noteable) @noteable = noteable @@ -15,7 +15,8 @@ module Projects [{ name: noteable.author.name, - username: noteable.author.username + username: noteable.author.username, + avatar_url: noteable.author.avatar_url }] end @@ -28,14 +29,14 @@ module Projects def sorted(users) users.uniq.to_a.compact.sort_by(&:username).map do |user| - { username: user.username, name: user.name } + { username: user.username, name: user.name, avatar_url: user.avatar_url } end end def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: group.name, count: count } + { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url } end end diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index f6e0b0a7c8a..6e5dd1b196d 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -76,6 +76,16 @@ script: - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" %h5.prepend-top-default + Use webhook + + %p.light + Add the following webhook to another project for Push and Tag push events. + The project will be rebuilt at the corresponding event. + + %pre + :plain + #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN + %h5.prepend-top-default Pass build variables %p.light @@ -83,10 +93,18 @@ %code variables[VARIABLE]=VALUE to an API request. Variable values can be used to distinguish between triggered builds and normal builds. - %pre.append-bottom-0 + With cURL: + + %pre :plain curl -X POST \ -F token=TOKEN \ -F "ref=REF_NAME" \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ #{builds_trigger_url(@project.id)} + %p.light + With webhook: + + %pre.append-bottom-0 + :plain + #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3176af9c19b..2fe9e82194b 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,3 +1,4 @@ +- form = local_assigns.fetch(:f) - project = @target_project || @project = form_errors(issuable) @@ -10,44 +11,17 @@ and make sure your changes will not unintentionally remove theirs .form-group - = f.label :title, class: 'control-label' + = form.label :title, class: 'control-label' = render 'shared/issuable/form/template_selector', issuable: issuable - - %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } - = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', - class: 'form-control pad', required: true - - - if issuable.is_a?(MergeRequest) - %p.help-block - .js-wip-explanation - %a.js-toggle-wip{href: "", tabindex: -1} - Remove the - %code WIP: - prefix from the title - to allow this - %strong Work In Progress - merge request to be merged when it's ready. - .js-no-wip-explanation - %a.js-toggle-wip{href: "", tabindex: -1} - Start the title with - %code WIP: - to prevent a - %strong Work In Progress - merge request from being merged before it's ready. - - - if can_add_template?(issuable) - %p.help-block - Add - = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1 - to help your contributors communicate effectively! + = render 'shared/issuable/form/title', issuable: issuable, form: form .form-group.detail-page-description - = f.label :description, 'Description', class: 'control-label' + = form.label :description, 'Description', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, + = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", supports_slash_commands: !issuable.persisted? @@ -59,8 +33,8 @@ .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :confidential do - = f.check_box :confidential + = form.label :confidential do + = form.check_box :confidential This issue is confidential and should only be visible to team members with at least Reporter access. - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) @@ -69,32 +43,32 @@ .row %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } .form-group.issue-assignee - = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - if issuable.assignee_id - = f.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) .form-group.issue-milestone - = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? - = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = f.hidden_field :label_ids, multiple: true, value: '' + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" - if has_due_date .col-lg-6 .form-group - = f.label :due_date, "Due date", class: "control-label" + = form.label :due_date, "Due date", class: "control-label" .col-sm-10 .issuable-form-select-holder - = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" - if issuable.can_move?(current_user) %hr @@ -112,15 +86,15 @@ %hr - if @merge_request.new_record? .form-group - = f.label :source_branch, class: 'control-label' + = form.label :source_branch, class: 'control-label' .col-sm-10 .issuable-form-select-holder - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + = form.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group - = f.label :target_branch, class: 'control-label' + = form.label :target_branch, class: 'control-label' .col-sm-10 .issuable-form-select-holder - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) + = form.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - if @merge_request.new_record? = link_to 'Change branches', mr_change_branches_path(@merge_request) @@ -136,9 +110,9 @@ - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if issuable.new_record? - = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else - = f.submit 'Save changes', class: 'btn btn-save' + = form.submit 'Save changes', class: 'btn btn-save' - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) .inline.prepend-left-10 @@ -155,4 +129,4 @@ method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' -= f.hidden_field :lock_version += form.hidden_field :lock_version diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml new file mode 100644 index 00000000000..83efdc7c8f7 --- /dev/null +++ b/app/views/shared/issuable/form/_title.html.haml @@ -0,0 +1,32 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) +- no_issuable_templates = issuable_templates(issuable).empty? +- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8' + +%div{ class: div_class } + = form.text_field :title, required: true, maxlength: 255, autofocus: true, + autocomplete: 'off', class: 'form-control pad' + + - if issuable.respond_to?(:work_in_progress?) + %p.help-block + .js-wip-explanation + %a.js-toggle-wip{ href: '', tabindex: -1 } + Remove the + %code WIP: + prefix from the title + to allow this + %strong Work In Progress + merge request to be merged when it's ready. + .js-no-wip-explanation + %a.js-toggle-wip{ href: '', tabindex: -1 } + Start the title with + %code WIP: + to prevent a + %strong Work In Progress + merge request from being merged before it's ready. + + - if no_issuable_templates && can?(current_user, :push_code, issuable.project) + %p.help-block + Add + = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 + to help your contributors communicate effectively! diff --git a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml new file mode 100644 index 00000000000..8f03746ff80 --- /dev/null +++ b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml @@ -0,0 +1,4 @@ +--- +title: Add setting to only allow merge requests to be merged when all discussions are resolved +merge_request: 7125 +author: Rodolfo Arruda diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml new file mode 100644 index 00000000000..95d8fef1099 --- /dev/null +++ b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml @@ -0,0 +1,4 @@ +--- +title: Use the Gitlab Workhorse HTTP header in the admin dashboard +merge_request: +author: Chris Wright diff --git a/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml b/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml new file mode 100644 index 00000000000..3af746cd92a --- /dev/null +++ b/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml @@ -0,0 +1,4 @@ +--- +title: Add link to build pipeline within individual build pages +merge_request: 7082 +author: diff --git a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml new file mode 100644 index 00000000000..1da72142880 --- /dev/null +++ b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml @@ -0,0 +1,4 @@ +--- +title: 'Fix: Todos Filter Shows All Users' +merge_request: +author: diff --git a/changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml b/changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml new file mode 100644 index 00000000000..95fd07c12e1 --- /dev/null +++ b/changelogs/unreleased/22680-unlabel-limit-autocomplete-to-selected-items.yml @@ -0,0 +1,4 @@ +--- +title: Limit autocomplete to currently selected items for unlabel slash command +merge_request: 22680 +author: Akram Fares diff --git a/changelogs/unreleased/22699-group-permssion-background-migration.yml b/changelogs/unreleased/22699-group-permssion-background-migration.yml new file mode 100644 index 00000000000..e8c221b6c42 --- /dev/null +++ b/changelogs/unreleased/22699-group-permssion-background-migration.yml @@ -0,0 +1,4 @@ +--- +title: Fix project records with invalid visibility_level values +merge_request: 7391 +author: diff --git a/changelogs/unreleased/22790-mention-autocomplete-avatar.yml b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml new file mode 100644 index 00000000000..53068ca5607 --- /dev/null +++ b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml @@ -0,0 +1,4 @@ +--- +title: Show avatars in mention dropdown +merge_request: 6865 +author: diff --git a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml new file mode 100644 index 00000000000..2312afdb3d7 --- /dev/null +++ b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml @@ -0,0 +1,4 @@ +--- +title: Issues atom feed url reflect filters on dashboard +merge_request: 7114 +author: Lucas Deschamps diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml new file mode 100644 index 00000000000..7b54d3df56d --- /dev/null +++ b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml @@ -0,0 +1,4 @@ +--- +title: Rewrite git blame spinach feature tests to rspec feature tests +merge_request: 7197 +author: Lisanne Fellinger diff --git a/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml new file mode 100644 index 00000000000..59e0d851366 --- /dev/null +++ b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml @@ -0,0 +1,4 @@ +--- +title: Make it possible to trigger builds from webhooks +merge_request: 7022 +author: Dmitry Poray diff --git a/changelogs/unreleased/23731-add-param-to-user-api.yml b/changelogs/unreleased/23731-add-param-to-user-api.yml new file mode 100644 index 00000000000..e31029ffb27 --- /dev/null +++ b/changelogs/unreleased/23731-add-param-to-user-api.yml @@ -0,0 +1,4 @@ +--- +title: Add query param to filter users by external & blocked type +merge_request: 7109 +author: Yatish Mehta diff --git a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml new file mode 100644 index 00000000000..b3bfcbda4b7 --- /dev/null +++ b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml @@ -0,0 +1,4 @@ +--- +title: Only skip group when it's actually a group in the "Share with group" select +merge_request: 7262 +author: diff --git a/changelogs/unreleased/24038-fix-no-register-pane-if-ldap.yml b/changelogs/unreleased/24038-fix-no-register-pane-if-ldap.yml new file mode 100644 index 00000000000..53f418b6b18 --- /dev/null +++ b/changelogs/unreleased/24038-fix-no-register-pane-if-ldap.yml @@ -0,0 +1,4 @@ +--- +title: Fix no "Register" tab if ldap auth is enabled (#24038) +merge_request: 7274 +author: Luc Didry diff --git a/changelogs/unreleased/24048-dropdown-issue-with-devider.yml b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml new file mode 100644 index 00000000000..b889da61957 --- /dev/null +++ b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml @@ -0,0 +1,4 @@ +--- +title: "[Fix] Extra divider issue in dropdown" +merge_request: 7398 +author: diff --git a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml new file mode 100644 index 00000000000..8ca0c5beab3 --- /dev/null +++ b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml @@ -0,0 +1,4 @@ +--- +title: 'Fix: Guest sees some repository details and gets 404' +merge_request: +author: diff --git a/changelogs/unreleased/24059-round-robin-repository-storage.yml b/changelogs/unreleased/24059-round-robin-repository-storage.yml new file mode 100644 index 00000000000..109536114ff --- /dev/null +++ b/changelogs/unreleased/24059-round-robin-repository-storage.yml @@ -0,0 +1,4 @@ +--- +title: Introduce round-robin project creation to spread load over multiple shards +merge_request: 7266 +author: diff --git a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml new file mode 100644 index 00000000000..50d018170f1 --- /dev/null +++ b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml @@ -0,0 +1,4 @@ +--- +title: Ensure merge request's "remove branch" accessors return booleans +merge_request: 7267 +author: diff --git a/changelogs/unreleased/24255-search-fix.yml b/changelogs/unreleased/24255-search-fix.yml new file mode 100644 index 00000000000..c0afade9bc8 --- /dev/null +++ b/changelogs/unreleased/24255-search-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken commits search +merge_request: +author: diff --git a/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml new file mode 100644 index 00000000000..72e7110d1b8 --- /dev/null +++ b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml @@ -0,0 +1,4 @@ +--- +title: Removed gray button styling from todo buttons in sidebars +merge_request: 7387 +author: diff --git a/changelogs/unreleased/24369-remove-additional-padding.yml b/changelogs/unreleased/24369-remove-additional-padding.yml new file mode 100644 index 00000000000..a6a0b248412 --- /dev/null +++ b/changelogs/unreleased/24369-remove-additional-padding.yml @@ -0,0 +1,4 @@ +--- +title: Remove additional padding on right-aligned items in MR widget. +merge_request: 7411 +author: Didem Acet diff --git a/changelogs/unreleased/24397-load-labels-on-mr-tabs.yml b/changelogs/unreleased/24397-load-labels-on-mr-tabs.yml new file mode 100644 index 00000000000..6bfa7fa1a49 --- /dev/null +++ b/changelogs/unreleased/24397-load-labels-on-mr-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Fix issue causing Labels not to appear in sidebar on MR page +merge_request: 7416 +author: Alex Sanford diff --git a/changelogs/unreleased/24492-promise-polyfill.yml b/changelogs/unreleased/24492-promise-polyfill.yml new file mode 100644 index 00000000000..d2fddfd83c6 --- /dev/null +++ b/changelogs/unreleased/24492-promise-polyfill.yml @@ -0,0 +1,4 @@ +--- +title: Adds es6-promise Polyfill +merge_request: 7482 +author: diff --git a/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml new file mode 100644 index 00000000000..c83558f33d1 --- /dev/null +++ b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml @@ -0,0 +1,4 @@ +--- +title: Fix expanding a collapsed diff when converting a symlink to a regular file +merge_request: 6953 +author: diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml new file mode 100644 index 00000000000..3af4f5e677d --- /dev/null +++ b/changelogs/unreleased/add-api-label-id.yml @@ -0,0 +1,4 @@ +--- +title: Expose label IDs in API +merge_request: 7275 +author: Rares Sfirlogea diff --git a/changelogs/unreleased/add-project-import-data-index.yml b/changelogs/unreleased/add-project-import-data-index.yml new file mode 100644 index 00000000000..f5e4005f544 --- /dev/null +++ b/changelogs/unreleased/add-project-import-data-index.yml @@ -0,0 +1,4 @@ +--- +title: Add an index for project_id in project_import_data to improve performance +merge_request: +author: diff --git a/changelogs/unreleased/always-show-download-button.yml b/changelogs/unreleased/always-show-download-button.yml new file mode 100644 index 00000000000..3a625834d01 --- /dev/null +++ b/changelogs/unreleased/always-show-download-button.yml @@ -0,0 +1,4 @@ +--- +title: Project download buttons always show +merge_request: 7405 +author: Philip Karpiak diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml new file mode 100644 index 00000000000..d703f8d32f3 --- /dev/null +++ b/changelogs/unreleased/api-label-priorities.yml @@ -0,0 +1,4 @@ +--- +title: "API: Ability to retrieve version information" +merge_request: 7286 +author: Robert Schilling diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml new file mode 100644 index 00000000000..d132d7e79c3 --- /dev/null +++ b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml @@ -0,0 +1,4 @@ +--- +title: Return 400 when creating a system hook fails +merge_request: 7350 +author: Robert Schilling diff --git a/changelogs/unreleased/broken-link-frontend-dev-guide.yml b/changelogs/unreleased/broken-link-frontend-dev-guide.yml new file mode 100644 index 00000000000..d7b6f4a7701 --- /dev/null +++ b/changelogs/unreleased/broken-link-frontend-dev-guide.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken link to observatory cli on Frontend Dev Guide +merge_request: +author: Sam Rose diff --git a/changelogs/unreleased/faster_project_search.yml b/changelogs/unreleased/faster_project_search.yml new file mode 100644 index 00000000000..e29a9f34ed4 --- /dev/null +++ b/changelogs/unreleased/faster_project_search.yml @@ -0,0 +1,4 @@ +--- +title: Faster search inside Project +merge_request: +author: diff --git a/changelogs/unreleased/feature-api_owned_resource.yml b/changelogs/unreleased/feature-api_owned_resource.yml new file mode 100644 index 00000000000..9c270e4ecf4 --- /dev/null +++ b/changelogs/unreleased/feature-api_owned_resource.yml @@ -0,0 +1,4 @@ +--- +title: Add api endpoint `/groups/owned` +merge_request: 7103 +author: Borja Aparicio diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml new file mode 100644 index 00000000000..d1bc8ea2eb1 --- /dev/null +++ b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml @@ -0,0 +1,4 @@ +--- +title: Fix 404 on network page when entering non-existent git revision +merge_request: 7172 +author: Hiroyuki Sato diff --git a/changelogs/unreleased/fix-cache-for-commit-status.yml b/changelogs/unreleased/fix-cache-for-commit-status.yml new file mode 100644 index 00000000000..eb4e96e75ae --- /dev/null +++ b/changelogs/unreleased/fix-cache-for-commit-status.yml @@ -0,0 +1,4 @@ +--- +title: Fix cache for commit status in commits list to respect branches +merge_request: 7372 +author: diff --git a/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml new file mode 100644 index 00000000000..ad6aa214f0f --- /dev/null +++ b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml @@ -0,0 +1,4 @@ +--- +title: Fix error when using invalid branch name when creating a new pipeline +merge_request: 7324 +author: diff --git a/changelogs/unreleased/fix-help-page-links.yml b/changelogs/unreleased/fix-help-page-links.yml new file mode 100644 index 00000000000..9e5f41c553f --- /dev/null +++ b/changelogs/unreleased/fix-help-page-links.yml @@ -0,0 +1,4 @@ +--- +title: Fix error links in help index page +merge_request: 7396 +author: Fu Xu diff --git a/changelogs/unreleased/fix-invalid-filename-eslint.yml b/changelogs/unreleased/fix-invalid-filename-eslint.yml new file mode 100644 index 00000000000..eea21149c90 --- /dev/null +++ b/changelogs/unreleased/fix-invalid-filename-eslint.yml @@ -0,0 +1,4 @@ +--- +title: Fix invalid filename validation on eslint +merge_request: 7281 +author: diff --git a/changelogs/unreleased/fix-search-input-padding.yml b/changelogs/unreleased/fix-search-input-padding.yml new file mode 100644 index 00000000000..5d559d05d73 --- /dev/null +++ b/changelogs/unreleased/fix-search-input-padding.yml @@ -0,0 +1,4 @@ +--- +title: Give search-input correct padding-right value +merge_request: 7407 +author: Philip Karpiak diff --git a/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml new file mode 100644 index 00000000000..8b41063151b --- /dev/null +++ b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml @@ -0,0 +1,4 @@ +--- +title: Clicking "force remove source branch" label now toggles the checkbox again +merge_request: +author: diff --git a/changelogs/unreleased/forking-in-progress-title.yml b/changelogs/unreleased/forking-in-progress-title.yml new file mode 100644 index 00000000000..4b9684844b3 --- /dev/null +++ b/changelogs/unreleased/forking-in-progress-title.yml @@ -0,0 +1,4 @@ +--- +title: Use 'Forking in progress' title when appropriate +merge_request: 7394 +author: Philip Karpiak diff --git a/changelogs/unreleased/git-gc-improvements.yml b/changelogs/unreleased/git-gc-improvements.yml new file mode 100644 index 00000000000..f15e667ce87 --- /dev/null +++ b/changelogs/unreleased/git-gc-improvements.yml @@ -0,0 +1,4 @@ +--- +title: Finer-grained Git gargage collection +merge_request: 6588 +author: diff --git a/changelogs/unreleased/issue-13823.yml b/changelogs/unreleased/issue-13823.yml new file mode 100644 index 00000000000..c1b5760f7df --- /dev/null +++ b/changelogs/unreleased/issue-13823.yml @@ -0,0 +1,4 @@ +--- +title: Show random messages when the To Do list is empty +merge_request: 6818 +author: Josep Llaneras diff --git a/changelogs/unreleased/issue_20245.yml b/changelogs/unreleased/issue_20245.yml new file mode 100644 index 00000000000..e5d09d85683 --- /dev/null +++ b/changelogs/unreleased/issue_20245.yml @@ -0,0 +1,4 @@ +--- +title: Fix project Visibility Level selector not using default values +merge_request: +author: diff --git a/changelogs/unreleased/issue_23032.yml b/changelogs/unreleased/issue_23032.yml new file mode 100644 index 00000000000..d376cf52112 --- /dev/null +++ b/changelogs/unreleased/issue_23032.yml @@ -0,0 +1,4 @@ +--- +title: Allow to test JIRA service settings without having a repository +merge_request: +author: diff --git a/changelogs/unreleased/ldap_check_bind.yml b/changelogs/unreleased/ldap_check_bind.yml new file mode 100644 index 00000000000..daff8103a07 --- /dev/null +++ b/changelogs/unreleased/ldap_check_bind.yml @@ -0,0 +1,4 @@ +--- +title: Introduce better credential and error checking to `rake gitlab:ldap:check` +merge_request: 6601 +author: diff --git a/changelogs/unreleased/milestone-project-require.yml b/changelogs/unreleased/milestone-project-require.yml new file mode 100644 index 00000000000..e43033541c7 --- /dev/null +++ b/changelogs/unreleased/milestone-project-require.yml @@ -0,0 +1,4 @@ +--- +title: Require projects before creating milestone. +merge_request: 7301 +author: gfyoung diff --git a/changelogs/unreleased/new-note-worker-record-not-found-fix.yml b/changelogs/unreleased/new-note-worker-record-not-found-fix.yml new file mode 100644 index 00000000000..abfba640cc0 --- /dev/null +++ b/changelogs/unreleased/new-note-worker-record-not-found-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix record not found error on NewNoteWorker processing +merge_request: 6863 +author: Oswaldo Ferreira diff --git a/changelogs/unreleased/pipeline-notifications.yml b/changelogs/unreleased/pipeline-notifications.yml new file mode 100644 index 00000000000..b43060674b2 --- /dev/null +++ b/changelogs/unreleased/pipeline-notifications.yml @@ -0,0 +1,6 @@ +--- +title: Add CI notifications. Who triggered a pipeline would receive an email after + the pipeline is succeeded or failed. Users could also update notification settings + accordingly +merge_request: 6342 +author: diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml new file mode 100644 index 00000000000..9f596e6a584 --- /dev/null +++ b/changelogs/unreleased/process-commits-using-sidekiq.yml @@ -0,0 +1,4 @@ +--- +title: Process commits using a dedicated Sidekiq worker +merge_request: 6802 +author: diff --git a/changelogs/unreleased/remove-heading-space-from-diff-content.yml b/changelogs/unreleased/remove-heading-space-from-diff-content.yml new file mode 100644 index 00000000000..1ea85784d29 --- /dev/null +++ b/changelogs/unreleased/remove-heading-space-from-diff-content.yml @@ -0,0 +1,4 @@ +--- +title: Remove an extra leading space from diff paste data +merge_request: 7133 +author: Hiroyuki Sato diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml new file mode 100644 index 00000000000..17cd5a993dd --- /dev/null +++ b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml @@ -0,0 +1,4 @@ +--- +title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2 +merge_request: +author: diff --git a/changelogs/unreleased/show-status-from-branch.yml b/changelogs/unreleased/show-status-from-branch.yml new file mode 100644 index 00000000000..1afc230c05c --- /dev/null +++ b/changelogs/unreleased/show-status-from-branch.yml @@ -0,0 +1,4 @@ +--- +title: Fix showing pipeline status for a given commit from correct branch +merge_request: 7034 +author: diff --git a/changelogs/unreleased/sidekiq-job-throttling.yml b/changelogs/unreleased/sidekiq-job-throttling.yml new file mode 100644 index 00000000000..ec4e2051c7e --- /dev/null +++ b/changelogs/unreleased/sidekiq-job-throttling.yml @@ -0,0 +1,4 @@ +--- +title: Added ability to throttle Sidekiq Jobs +merge_request: 7292 +author: diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml new file mode 100644 index 00000000000..3df2a415dbc --- /dev/null +++ b/changelogs/unreleased/sidekiq_default_retries.yml @@ -0,0 +1,4 @@ +--- +title: Set default Sidekiq retries to 3 +merge_request: 7294 +author: diff --git a/changelogs/unreleased/upgrade-timeago.yml b/changelogs/unreleased/upgrade-timeago.yml new file mode 100644 index 00000000000..ddb266ba558 --- /dev/null +++ b/changelogs/unreleased/upgrade-timeago.yml @@ -0,0 +1,4 @@ +--- +title: Replace jQuery.timeago with timeago.js +merge_request: 6274 +author: ClemMakesApps diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml new file mode 100644 index 00000000000..e498f8dd0a6 --- /dev/null +++ b/changelogs/unreleased/use-separate-token-for-incoming-email.yml @@ -0,0 +1,4 @@ +--- +title: Use separate email-token for incoming email and revert back the inactive feature +merge_request: 5914 +author: diff --git a/changelogs/unreleased/user_filter_auth.yml b/changelogs/unreleased/user_filter_auth.yml new file mode 100644 index 00000000000..e4071e22e5e --- /dev/null +++ b/changelogs/unreleased/user_filter_auth.yml @@ -0,0 +1,4 @@ +--- +title: Centralize LDAP config/filter logic +merge_request: 6606 +author: diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index fd23047f027..d3f216fb3bf 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -257,6 +257,24 @@ the LDAP server's SSL certificate is performed. ## Troubleshooting +### Debug LDAP user filter with ldapsearch + +This example uses ldapsearch and assumes you are using ActiveDirectory. The +following query returns the login names of the users that will be allowed to +log in to GitLab if you configure your own user_filter. + +``` +ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "$user_filter" sAMAccountName +``` + +- Variables beginning with a `$` refer to a variable from the LDAP section of + your configuration file. +- Replace ldaps:// with ldap:// if you are using the plain authentication method. + Port `389` is the default `ldap://` port and `636` is the default `ldaps://` + port. +- We are assuming the password for the bind_dn user is in bind_dn_password.txt. + + ### Invalid credentials when logging in - Make sure the user you are binding with has enough permissions to read the user's diff --git a/doc/api/builds.md b/doc/api/builds.md index 0476cac0eda..bca2f9e44ef 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -45,7 +45,7 @@ Example of response "ref": "master", "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "status": "pending" - } + }, "ref": "master", "runner": null, "stage": "test", @@ -89,7 +89,7 @@ Example of response "ref": "master", "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "status": "pending" - } + }, "ref": "master", "runner": null, "stage": "test", @@ -163,7 +163,7 @@ Example of response "ref": "master", "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "status": "pending" - } + }, "ref": "master", "runner": null, "stage": "test", @@ -193,7 +193,7 @@ Example of response "ref": "master", "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "status": "pending" - } + }, "ref": "master", "runner": null, "stage": "test", @@ -260,7 +260,7 @@ Example of response "ref": "master", "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "status": "pending" - } + }, "ref": "master", "runner": null, "stage": "test", diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 84048f1d25f..cf7c55f75f2 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -58,6 +58,22 @@ below. See the [Examples](#examples) section for more details on how to actually trigger a rebuild. +## Trigger a build from webhook + +> Introduced in GitLab 8.14. + +To trigger a build from webhook of another project you need to add the following +webhook url for Push and Tag push events: + +``` +https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN +``` + +> **Note**: +- `ref` should be passed as part of url in order to take precedence over `ref` + from webhook body that designates the branchref that fired the trigger in the source repository. +- `ref` should be url encoded if contains slashes. + ## Pass build variables to a trigger You can pass any number of arbitrary variables in the trigger API call and they @@ -169,6 +185,14 @@ curl --request POST \ https://gitlab.example.com/api/v3/projects/9/trigger/builds ``` +### Using webhook to trigger builds + +You can add the following webhook to another project in order to trigger a build: + +``` +https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true +``` + ### Using cron to trigger nightly builds Whether you craft a script or just run cURL directly, you can trigger builds diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md new file mode 100644 index 00000000000..03392a003ee --- /dev/null +++ b/doc/development/ux_guide/copy.md @@ -0,0 +1,78 @@ +# Copy + +The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab. + +>**Note:** +We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align. + +## Contents +* [Brevity](#brevity) +* [Forms](#forms) +* [Terminology](#terminology) + +--- + +## Brevity +Users will skim content, rather than read text carefully. +When familiar with a web app, users rely on muscle memory, and may read even less when moving quickly. +A good experience should quickly orient a user, regardless of their experience, to the purpose of the current screen. This should happen without the user having to consciously read long strings of text. +In general, text is burdensome and adds cognitive load. This is especially pronounced in a powerful productivity tool such as GitLab. +We should _not_ rely on words as a crutch to explain the purpose of a screen. +The current navigation and composition of the elements on the screen should get the user 95% there, with the remaining 5% being specific elements such as text. +This means that, as a rule, copy should be very short. A long message or label is a red flag hinting at design that needs improvement. + +>**Example:** +Use `Add` instead of `Add issue` as a button label. +Preferrably use context and placement of controls to make it obvious what clicking on them will do. + +--- + +## Forms + +### Adding items + +When viewing a list of issues, there is a button that is labeled `Add`. Given the context in the example, it is clearly referring to issues. If the context were not clear enough, the label could be `Add issue`. Clicking the button will bring you to the `Add issue` form. Other add flows should be similar. + +![Add issue button](img/copy-form-addissuebutton.png) + +The form should be titled `Add issue`. The submit button should be labeled `Save` or `Submit`. Do not use `Add`, `Create`, `New`, or `Save Changes`. The cancel button should be labeled `Cancel`. Do not use `Back`. + +![Add issue form](img/copy-form-addissueform.png) + +### Editing items + +When in context of an issue, the affordance to edit it is labeled `Edit`. If the context is not clear enough, `Edit issue` could be considered. Other edit flows should be similar. + +![Edit issue button](img/copy-form-editissuebutton.png) + +The form should be titled `Edit Issue`. The submit button should be labeled `Save`. Do not use `Edit`, `Update`, `New`, or `Save Changes`. The cancel button should be labeled `Cancel`. Do not use `Back`. + +![Edit issue form](img/copy-form-editissueform.png) + +--- + +## Terminology + +### Issues + +#### Adjectives (states) + +| Term | Use | +| ---- | --- | +| Open | Issue is active | +| Closed | Issue is no longer active | + +>**Example:** +Use `5 open issues` and do not use `5 pending issues`. +Only use the adjectives in the table above. + +#### Verbs (actions) + +| Term | Use | +| ---- | --- | +| Add | For adding an issue. Do not use `create` or `new` | +| View | View an issue | +| Edit | Edit an issue. Do not use `update` | +| Close | Closing an issue | +| Re-open | Re-open an issue. There should never be a need to use `open` as a verb | +| Delete | Deleting an issue. Do not use `remove` |
\ No newline at end of file diff --git a/doc/development/ux_guide/img/copy-form-addissuebutton.png b/doc/development/ux_guide/img/copy-form-addissuebutton.png Binary files differnew file mode 100644 index 00000000000..18839d447e8 --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-addissuebutton.png diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png Binary files differnew file mode 100644 index 00000000000..e6838c06eca --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-addissueform.png diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png Binary files differnew file mode 100644 index 00000000000..2435820e14f --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-editissuebutton.png diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png Binary files differnew file mode 100644 index 00000000000..5ddeda33e68 --- /dev/null +++ b/doc/development/ux_guide/img/copy-form-editissueform.png diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md index 1a61be4ed51..8aed11ebac3 100644 --- a/doc/development/ux_guide/index.md +++ b/doc/development/ux_guide/index.md @@ -26,6 +26,11 @@ The GitLab experience is broken apart into several surfaces. Each of these surfa --- +### [Copy](copy.md) +Conventions on text and messaging within labels, buttons, and other components. + +--- + ### [Features](features.md) The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states. diff --git a/lib/api/notes.rb b/lib/api/notes.rb index c5c214d4d13..b255b47742b 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -5,23 +5,23 @@ module API NOTEABLE_TYPES = [Issue, MergeRequest, Snippet] + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do NOTEABLE_TYPES.each do |noteable_type| noteables_str = noteable_type.to_s.underscore.pluralize - noteable_id_str = "#{noteable_type.to_s.underscore}_id" - - # Get a list of project +noteable+ notes - # - # Parameters: - # id (required) - The ID of a project - # noteable_id (required) - The ID of an issue or snippet - # Example Request: - # GET /projects/:id/issues/:noteable_id/notes - # GET /projects/:id/snippets/:noteable_id/notes - get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do - @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) - - if can?(current_user, noteable_read_ability_name(@noteable), @noteable) + + desc 'Get a list of project +noteable+ notes' do + success Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + end + get ":id/#{noteables_str}/:noteable_id/notes" do + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + + if can?(current_user, noteable_read_ability_name(noteable), noteable) # We exclude notes that are cross-references and that cannot be viewed # by the current user. By doing this exclusion at this level and not # at the DB query level (which we cannot in that case), the current @@ -31,7 +31,7 @@ module API # paginate() only works with a relation. This could lead to a # mismatch between the pagination headers info and the actual notes # array returned, but this is really a edge-case. - paginate(@noteable.notes). + paginate(noteable.notes). reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note else @@ -39,44 +39,40 @@ module API end end - # Get a single +noteable+ note - # - # Parameters: - # id (required) - The ID of a project - # noteable_id (required) - The ID of an issue or snippet - # note_id (required) - The ID of a note - # Example Request: - # GET /projects/:id/issues/:noteable_id/notes/:note_id - # GET /projects/:id/snippets/:noteable_id/notes/:note_id - get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do - @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) - @note = @noteable.notes.find(params[:note_id]) - can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) + desc 'Get a single +noteable+ note' do + success Entities::Note + end + params do + requires :note_id, type: Integer, desc: 'The ID of a note' + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + end + get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + note = noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) if can_read_note - present @note, with: Entities::Note + present note, with: Entities::Note else not_found!("Note") end end - # Create a new +noteable+ note - # - # Parameters: - # id (required) - The ID of a project - # noteable_id (required) - The ID of an issue or snippet - # body (required) - The content of a note - # created_at (optional) - The date - # Example Request: - # POST /projects/:id/issues/:noteable_id/notes - # POST /projects/:id/snippets/:noteable_id/notes - post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do + desc 'Create a new +noteable+ note' do + success Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :body, type: String, desc: 'The content of a note' + optional :created_at, type: String, desc: 'The creation date of the note' + end + post ":id/#{noteables_str}/:noteable_id/notes" do required_attributes! [:body] opts = { note: params[:body], noteable_type: noteables_str.classify, - noteable_id: params[noteable_id_str] + noteable_id: params[:noteable_id] } if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) @@ -92,19 +88,15 @@ module API end end - # Modify existing +noteable+ note - # - # Parameters: - # id (required) - The ID of a project - # noteable_id (required) - The ID of an issue or snippet - # node_id (required) - The ID of a note - # body (required) - New content of a note - # Example Request: - # PUT /projects/:id/issues/:noteable_id/notes/:note_id - # PUT /projects/:id/snippets/:noteable_id/notes/:node_id - put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do - required_attributes! [:body] - + desc 'Update an existing +noteable+ note' do + success Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :note_id, type: Integer, desc: 'The ID of a note' + requires :body, type: String, desc: 'The content of a note' + end + put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do note = user_project.notes.find(params[:note_id]) authorize! :admin_note, note @@ -113,25 +105,23 @@ module API note: params[:body] } - @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) + note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) - if @note.valid? - present @note, with: Entities::Note + if note.valid? + present note, with: Entities::Note else render_api_error!("Failed to save note #{note.errors.messages}", 400) end end - # Delete a +noteable+ note - # - # Parameters: - # id (required) - The ID of a project - # noteable_id (required) - The ID of an issue, MR, or snippet - # node_id (required) - The ID of a note - # Example Request: - # DELETE /projects/:id/issues/:noteable_id/notes/:note_id - # DELETE /projects/:id/snippets/:noteable_id/notes/:node_id - delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do + desc 'Delete a +noteable+ note' do + success Entities::Note + end + params do + requires :noteable_id, type: Integer, desc: 'The ID of the noteable' + requires :note_id, type: Integer, desc: 'The ID of a note' + end + delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do note = user_project.notes.find(params[:note_id]) authorize! :admin_note, note diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 9a4f1cd342f..569598fbd2c 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -12,7 +12,7 @@ module API requires :token, type: String, desc: 'The unique token of trigger' optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end - post ":id/trigger/builds" do + post ":id/(ref/:ref/)trigger/builds" do project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 8ba2eccf66c..c890a51ae42 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -54,6 +54,13 @@ describe API::API do expect(pipeline.builds.size).to eq(5) end + it 'creates builds on webhook from other gitlab repository and branch' do + expect do + post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } + end.to change(project.builds, :count).by(5) + expect(response).to have_http_status(201) + end + it 'returns bad request with no builds created if there\'s no commit for that ref' do post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') expect(response).to have_http_status(400) diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js new file mode 100644 index 00000000000..19e6c13a655 --- /dev/null +++ b/vendor/assets/javascripts/es6-promise.auto.js @@ -0,0 +1,1159 @@ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version 4.0.5 + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.ES6Promise = factory()); +}(this, (function () { 'use strict'; + +function objectOrFunction(x) { + return typeof x === 'function' || typeof x === 'object' && x !== null; +} + +function isFunction(x) { + return typeof x === 'function'; +} + +var _isArray = undefined; +if (!Array.isArray) { + _isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; +} else { + _isArray = Array.isArray; +} + +var isArray = _isArray; + +var len = 0; +var vertxNext = undefined; +var customSchedulerFn = undefined; + +var asap = function asap(callback, arg) { + queue[len] = callback; + queue[len + 1] = arg; + len += 2; + if (len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (customSchedulerFn) { + customSchedulerFn(flush); + } else { + scheduleFlush(); + } + } +}; + +function setScheduler(scheduleFn) { + customSchedulerFn = scheduleFn; +} + +function setAsap(asapFn) { + asap = asapFn; +} + +var browserWindow = typeof window !== 'undefined' ? window : undefined; +var browserGlobal = browserWindow || {}; +var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; +var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]'; + +// test for web worker but not in IE10 +var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined'; + +// node +function useNextTick() { + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // see https://github.com/cujojs/when/issues/410 for details + return function () { + return process.nextTick(flush); + }; +} + +// vertx +function useVertxTimer() { + if (typeof vertxNext !== 'undefined') { + return function () { + vertxNext(flush); + }; + } + + return useSetTimeout(); +} + +function useMutationObserver() { + var iterations = 0; + var observer = new BrowserMutationObserver(flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function () { + node.data = iterations = ++iterations % 2; + }; +} + +// web worker +function useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = flush; + return function () { + return channel.port2.postMessage(0); + }; +} + +function useSetTimeout() { + // Store setTimeout reference so es6-promise will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + var globalSetTimeout = setTimeout; + return function () { + return globalSetTimeout(flush, 1); + }; +} + +var queue = new Array(1000); +function flush() { + for (var i = 0; i < len; i += 2) { + var callback = queue[i]; + var arg = queue[i + 1]; + + callback(arg); + + queue[i] = undefined; + queue[i + 1] = undefined; + } + + len = 0; +} + +function attemptVertx() { + try { + var r = require; + var vertx = r('vertx'); + vertxNext = vertx.runOnLoop || vertx.runOnContext; + return useVertxTimer(); + } catch (e) { + return useSetTimeout(); + } +} + +var scheduleFlush = undefined; +// Decide what async method to use to triggering processing of queued callbacks: +if (isNode) { + scheduleFlush = useNextTick(); +} else if (BrowserMutationObserver) { + scheduleFlush = useMutationObserver(); +} else if (isWorker) { + scheduleFlush = useMessageChannel(); +} else if (browserWindow === undefined && typeof require === 'function') { + scheduleFlush = attemptVertx(); +} else { + scheduleFlush = useSetTimeout(); +} + +function then(onFulfillment, onRejection) { + var _arguments = arguments; + + var parent = this; + + var child = new this.constructor(noop); + + if (child[PROMISE_ID] === undefined) { + makePromise(child); + } + + var _state = parent._state; + + if (_state) { + (function () { + var callback = _arguments[_state - 1]; + asap(function () { + return invokeCallback(_state, child, callback, parent._result); + }); + })(); + } else { + subscribe(parent, child, onFulfillment, onRejection); + } + + return child; +} + +/** + `Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @static + @param {Any} value value that the returned promise will be resolved with + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` +*/ +function resolve(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(noop); + _resolve(promise, object); + return promise; +} + +var PROMISE_ID = Math.random().toString(36).substring(16); + +function noop() {} + +var PENDING = void 0; +var FULFILLED = 1; +var REJECTED = 2; + +var GET_THEN_ERROR = new ErrorObject(); + +function selfFulfillment() { + return new TypeError("You cannot resolve a promise with itself"); +} + +function cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); +} + +function getThen(promise) { + try { + return promise.then; + } catch (error) { + GET_THEN_ERROR.error = error; + return GET_THEN_ERROR; + } +} + +function tryThen(then, value, fulfillmentHandler, rejectionHandler) { + try { + then.call(value, fulfillmentHandler, rejectionHandler); + } catch (e) { + return e; + } +} + +function handleForeignThenable(promise, thenable, then) { + asap(function (promise) { + var sealed = false; + var error = tryThen(then, thenable, function (value) { + if (sealed) { + return; + } + sealed = true; + if (thenable !== value) { + _resolve(promise, value); + } else { + fulfill(promise, value); + } + }, function (reason) { + if (sealed) { + return; + } + sealed = true; + + _reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + _reject(promise, error); + } + }, promise); +} + +function handleOwnThenable(promise, thenable) { + if (thenable._state === FULFILLED) { + fulfill(promise, thenable._result); + } else if (thenable._state === REJECTED) { + _reject(promise, thenable._result); + } else { + subscribe(thenable, undefined, function (value) { + return _resolve(promise, value); + }, function (reason) { + return _reject(promise, reason); + }); + } +} + +function handleMaybeThenable(promise, maybeThenable, then$$) { + if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) { + handleOwnThenable(promise, maybeThenable); + } else { + if (then$$ === GET_THEN_ERROR) { + _reject(promise, GET_THEN_ERROR.error); + } else if (then$$ === undefined) { + fulfill(promise, maybeThenable); + } else if (isFunction(then$$)) { + handleForeignThenable(promise, maybeThenable, then$$); + } else { + fulfill(promise, maybeThenable); + } + } +} + +function _resolve(promise, value) { + if (promise === value) { + _reject(promise, selfFulfillment()); + } else if (objectOrFunction(value)) { + handleMaybeThenable(promise, value, getThen(value)); + } else { + fulfill(promise, value); + } +} + +function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + publish(promise); +} + +function fulfill(promise, value) { + if (promise._state !== PENDING) { + return; + } + + promise._result = value; + promise._state = FULFILLED; + + if (promise._subscribers.length !== 0) { + asap(publish, promise); + } +} + +function _reject(promise, reason) { + if (promise._state !== PENDING) { + return; + } + promise._state = REJECTED; + promise._result = reason; + + asap(publishRejection, promise); +} + +function subscribe(parent, child, onFulfillment, onRejection) { + var _subscribers = parent._subscribers; + var length = _subscribers.length; + + parent._onerror = null; + + _subscribers[length] = child; + _subscribers[length + FULFILLED] = onFulfillment; + _subscribers[length + REJECTED] = onRejection; + + if (length === 0 && parent._state) { + asap(publish, parent); + } +} + +function publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { + return; + } + + var child = undefined, + callback = undefined, + detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; +} + +function ErrorObject() { + this.error = null; +} + +var TRY_CATCH_ERROR = new ErrorObject(); + +function tryCatch(callback, detail) { + try { + return callback(detail); + } catch (e) { + TRY_CATCH_ERROR.error = e; + return TRY_CATCH_ERROR; + } +} + +function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value = undefined, + error = undefined, + succeeded = undefined, + failed = undefined; + + if (hasCallback) { + value = tryCatch(callback, detail); + + if (value === TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value = null; + } else { + succeeded = true; + } + + if (promise === value) { + _reject(promise, cannotReturnOwn()); + return; + } + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== PENDING) { + // noop + } else if (hasCallback && succeeded) { + _resolve(promise, value); + } else if (failed) { + _reject(promise, error); + } else if (settled === FULFILLED) { + fulfill(promise, value); + } else if (settled === REJECTED) { + _reject(promise, value); + } +} + +function initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value) { + _resolve(promise, value); + }, function rejectPromise(reason) { + _reject(promise, reason); + }); + } catch (e) { + _reject(promise, e); + } +} + +var id = 0; +function nextId() { + return id++; +} + +function makePromise(promise) { + promise[PROMISE_ID] = id++; + promise._state = undefined; + promise._result = undefined; + promise._subscribers = []; +} + +function Enumerator(Constructor, input) { + this._instanceConstructor = Constructor; + this.promise = new Constructor(noop); + + if (!this.promise[PROMISE_ID]) { + makePromise(this.promise); + } + + if (isArray(input)) { + this._input = input; + this.length = input.length; + this._remaining = input.length; + + this._result = new Array(this.length); + + if (this.length === 0) { + fulfill(this.promise, this._result); + } else { + this.length = this.length || 0; + this._enumerate(); + if (this._remaining === 0) { + fulfill(this.promise, this._result); + } + } + } else { + _reject(this.promise, validationError()); + } +} + +function validationError() { + return new Error('Array Methods must be provided an Array'); +}; + +Enumerator.prototype._enumerate = function () { + var length = this.length; + var _input = this._input; + + for (var i = 0; this._state === PENDING && i < length; i++) { + this._eachEntry(_input[i], i); + } +}; + +Enumerator.prototype._eachEntry = function (entry, i) { + var c = this._instanceConstructor; + var resolve$$ = c.resolve; + + if (resolve$$ === resolve) { + var _then = getThen(entry); + + if (_then === then && entry._state !== PENDING) { + this._settledAt(entry._state, i, entry._result); + } else if (typeof _then !== 'function') { + this._remaining--; + this._result[i] = entry; + } else if (c === Promise) { + var promise = new c(noop); + handleMaybeThenable(promise, entry, _then); + this._willSettleAt(promise, i); + } else { + this._willSettleAt(new c(function (resolve$$) { + return resolve$$(entry); + }), i); + } + } else { + this._willSettleAt(resolve$$(entry), i); + } +}; + +Enumerator.prototype._settledAt = function (state, i, value) { + var promise = this.promise; + + if (promise._state === PENDING) { + this._remaining--; + + if (state === REJECTED) { + _reject(promise, value); + } else { + this._result[i] = value; + } + } + + if (this._remaining === 0) { + fulfill(promise, this._result); + } +}; + +Enumerator.prototype._willSettleAt = function (promise, i) { + var enumerator = this; + + subscribe(promise, undefined, function (value) { + return enumerator._settledAt(FULFILLED, i, value); + }, function (reason) { + return enumerator._settledAt(REJECTED, i, reason); + }); +}; + +/** + `Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = resolve(2); + let promise3 = resolve(3); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = reject(new Error("2")); + let promise3 = reject(new Error("3")); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @static + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static +*/ +function all(entries) { + return new Enumerator(this, entries).promise; +} + +/** + `Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 2'); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // result === 'promise 2' because it was resolved before promise1 + // was resolved. + }); + ``` + + `Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error('promise 2')); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === 'promise 2' because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @static + @param {Array} promises array of promises to observe + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. +*/ +function race(entries) { + /*jshint validthis:true */ + var Constructor = this; + + if (!isArray(entries)) { + return new Constructor(function (_, reject) { + return reject(new TypeError('You must pass an array to race.')); + }); + } else { + return new Constructor(function (resolve, reject) { + var length = entries.length; + for (var i = 0; i < length; i++) { + Constructor.resolve(entries[i]).then(resolve, reject); + } + }); + } +} + +/** + `Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @static + @param {Any} reason value that the returned promise will be rejected with. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. +*/ +function reject(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(noop); + _reject(promise, reason); + return promise; +} + +function needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); +} + +function needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); +} + +/** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + let promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + let xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {function} resolver + Useful for tooling. + @constructor +*/ +function Promise(resolver) { + this[PROMISE_ID] = nextId(); + this._result = this._state = undefined; + this._subscribers = []; + + if (noop !== resolver) { + typeof resolver !== 'function' && needsResolver(); + this instanceof Promise ? initializePromise(this, resolver) : needsNew(); + } +} + +Promise.all = all; +Promise.race = race; +Promise.resolve = resolve; +Promise.reject = reject; +Promise._setScheduler = setScheduler; +Promise._setAsap = setAsap; +Promise._asap = asap; + +Promise.prototype = { + constructor: Promise, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + let result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + let author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + then: then, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + 'catch': function _catch(onRejection) { + return this.then(null, onRejection); + } +}; + +function polyfill() { + var local = undefined; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P) { + var promiseToString = null; + try { + promiseToString = Object.prototype.toString.call(P.resolve()); + } catch (e) { + // silently ignored + } + + if (promiseToString === '[object Promise]' && !P.cast) { + return; + } + } + + local.Promise = Promise; +} + +// Strange compat.. +Promise.polyfill = polyfill; +Promise.Promise = Promise; + +return Promise; + +}))); + +ES6Promise.polyfill(); +//# sourceMappingURL=es6-promise.auto.map |