summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js193
-rw-r--r--app/assets/javascripts/gl_form.js4
-rw-r--r--app/assets/javascripts/issue.js74
-rw-r--r--app/assets/javascripts/notes.js217
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/issues.scss83
-rw-r--r--app/assets/stylesheets/pages/members.scss52
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss1
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/projects/artifacts_controller.rb8
-rw-r--r--app/controllers/projects/branches_controller.rb34
-rw-r--r--app/controllers/projects/issues_controller.rb21
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb15
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/helpers/blob_helper.rb24
-rw-r--r--app/helpers/notes_helper.rb24
-rw-r--r--app/helpers/tree_helper.rb4
-rw-r--r--app/models/blob.rb46
-rw-r--r--app/models/blob_viewer/base.rb11
-rw-r--r--app/models/concerns/blob_like.rb48
-rw-r--r--app/models/concerns/discussion_on_diff.rb1
-rw-r--r--app/models/concerns/note_on_diff.rb4
-rw-r--r--app/models/diff_discussion.rb20
-rw-r--r--app/models/diff_note.rb7
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/legacy_diff_discussion.rb18
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/note.rb22
-rw-r--r--app/models/project_services/chat_message/base_message.rb11
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb10
-rw-r--r--app/models/repository.rb42
-rw-r--r--app/models/snippet_blob.rb30
-rw-r--r--app/serializers/merge_request_create_entity.rb7
-rw-r--r--app/serializers/merge_request_create_serializer.rb3
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb54
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml4
-rw-r--r--app/views/projects/artifacts/browse.html.haml24
-rw-r--r--app/views/projects/blob/_blob.html.haml16
-rw-r--r--app/views/projects/builds/_header.html.haml28
-rw-r--r--app/views/projects/diffs/_line.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml36
-rw-r--r--app/views/projects/issues/show.html.haml7
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml14
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml4
-rw-r--r--app/views/projects/project_members/_index.html.haml6
-rw-r--r--app/views/projects/project_members/_team.html.haml10
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml7
-rw-r--r--app/views/shared/notes/_note.html.haml6
-rw-r--r--changelogs/unreleased/26883-members-page-layout-looks-broken.yml4
-rw-r--r--changelogs/unreleased/28558-create-new-branch-from-issue-page.yml4
-rw-r--r--changelogs/unreleased/30458-real-time-note-edits.yml4
-rw-r--r--changelogs/unreleased/31544-size-of-project-from-api.yml4
-rw-r--r--changelogs/unreleased/31647-fix-snippet-content_html.yml4
-rw-r--r--changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml4
-rw-r--r--changelogs/unreleased/dm-artifact-browser-header.yml4
-rw-r--r--changelogs/unreleased/dm-comment-on-diff-versions.yml4
-rw-r--r--changelogs/unreleased/zj-chat-message-pretty-time.yml4
-rw-r--r--config/routes/project.rb1
-rw-r--r--db/migrate/20170502091007_markdown_cache_limits_to_mysql.rb2
-rw-r--r--db/migrate/markdown_cache_limits_to_mysql.rb13
-rw-r--r--db/schema.rb2
-rw-r--r--doc/api/projects.md29
-rw-r--r--doc/workflow/img/todos_icon.pngbin1341 -> 4910 bytes
-rw-r--r--doc/workflow/img/todos_index.pngbin63372 -> 98239 bytes
-rw-r--r--features/project/builds/artifacts.feature2
-rw-r--r--features/steps/project/builds/artifacts.rb12
-rw-r--r--lib/api/projects.rb14
-rw-r--r--lib/gitlab/git/blob.rb12
-rw-r--r--lib/gitlab/git/repository.rb21
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/tasks/gitlab/db.rake1
-rw-r--r--lib/tasks/migrate/add_limits_mysql.rake2
-rwxr-xr-xscripts/static-analysis1
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb38
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb24
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb10
-rw-r--r--spec/features/issues/create_branch_merge_request_spec.rb91
-rw-r--r--spec/features/issues/new_branch_button_spec.rb62
-rw-r--r--spec/features/issues/note_polling_spec.rb75
-rw-r--r--spec/features/merge_requests/versions_spec.rb78
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb43
-rw-r--r--spec/features/protected_tags/access_control_ce_spec.rb4
-rw-r--r--spec/features/protected_tags_spec.rb1
-rw-r--r--spec/fixtures/api/schemas/branch.json12
-rw-r--r--spec/fixtures/api/schemas/merge_request.json12
-rw-r--r--spec/helpers/blob_helper_spec.rb48
-rw-r--r--spec/helpers/notes_helper_spec.rb139
-rw-r--r--spec/javascripts/issue_spec.js4
-rw-r--r--spec/javascripts/notes_spec.js131
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb4
-rw-r--r--spec/models/blob_spec.rb124
-rw-r--r--spec/models/blob_viewer/base_spec.rb6
-rw-r--r--spec/models/diff_discussion_spec.rb83
-rw-r--r--spec/models/diff_note_spec.rb52
-rw-r--r--spec/models/issue_spec.rb21
-rw-r--r--spec/models/legacy_diff_discussion_spec.rb22
-rw-r--r--spec/models/merge_request_spec.rb19
-rw-r--r--spec/models/note_spec.rb86
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb15
-rw-r--r--spec/models/repository_spec.rb26
-rw-r--r--spec/requests/api/projects_spec.rb14
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb74
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb18
109 files changed, 2190 insertions, 600 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index aa62a86d31d..7f665f19132 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -264,11 +264,15 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
+ <<: *except-docs
stage: test
script:
- scripts/static-analysis
-docs:check:links:
+# Documentation checks:
+# - Check validity of relative links
+# - Make sure cURL examples in API docs use the full switches
+docs lint:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
@@ -276,6 +280,7 @@ docs:check:links:
dependencies: []
before_script: []
script:
+ - scripts/lint-doc.sh
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
new file mode 100644
index 00000000000..ff2f2c81971
--- /dev/null
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -0,0 +1,193 @@
+/* eslint-disable no-new */
+/* global Flash */
+import DropLab from './droplab/drop_lab';
+import ISetter from './droplab/plugins/input_setter';
+
+// Todo: Remove this when fixing issue in input_setter plugin
+const InputSetter = Object.assign({}, ISetter);
+
+const CREATE_MERGE_REQUEST = 'create-mr';
+const CREATE_BRANCH = 'create-branch';
+
+export default class CreateMergeRequestDropdown {
+ constructor(wrapperEl) {
+ this.wrapperEl = wrapperEl;
+ this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
+ this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
+ this.availableButton = this.wrapperEl.querySelector('.available');
+ this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
+ this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
+ this.unavailableButtonText = this.unavailableButton.querySelector('.text');
+
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
+ this.createMrPath = this.wrapperEl.dataset.createMrPath;
+ this.droplabInitialized = false;
+ this.isCreatingMergeRequest = false;
+ this.mergeRequestCreated = false;
+ this.isCreatingBranch = false;
+ this.branchCreated = false;
+
+ this.init();
+ }
+
+ init() {
+ this.checkAbilityToCreateBranch();
+ }
+
+ available() {
+ this.availableButton.classList.remove('hide');
+ this.unavailableButton.classList.add('hide');
+ }
+
+ unavailable() {
+ this.availableButton.classList.add('hide');
+ this.unavailableButton.classList.remove('hide');
+ }
+
+ enable() {
+ this.createMergeRequestButton.classList.remove('disabled');
+ this.createMergeRequestButton.removeAttribute('disabled');
+
+ this.dropdownToggle.classList.remove('disabled');
+ this.dropdownToggle.removeAttribute('disabled');
+ }
+
+ disable() {
+ this.createMergeRequestButton.classList.add('disabled');
+ this.createMergeRequestButton.setAttribute('disabled', 'disabled');
+
+ this.dropdownToggle.classList.add('disabled');
+ this.dropdownToggle.setAttribute('disabled', 'disabled');
+ }
+
+ hide() {
+ this.wrapperEl.classList.add('hide');
+ }
+
+ setUnavailableButtonState(isLoading = true) {
+ if (isLoading) {
+ this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
+ this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = 'Checking branch availability…';
+ } else {
+ this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
+ this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = 'New branch unavailable';
+ }
+ }
+
+ checkAbilityToCreateBranch() {
+ return $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ url: this.canCreatePath,
+ beforeSend: () => this.setUnavailableButtonState(),
+ })
+ .done((data) => {
+ this.setUnavailableButtonState(false);
+
+ if (data.can_create_branch) {
+ this.available();
+ this.enable();
+
+ if (!this.droplabInitialized) {
+ this.droplabInitialized = true;
+ this.initDroplab();
+ this.bindEvents();
+ }
+ } else if (data.has_related_branch) {
+ this.hide();
+ }
+ }).fail(() => {
+ this.unavailable();
+ this.disable();
+ new Flash('Failed to check if a new branch can be created.');
+ });
+ }
+
+ initDroplab() {
+ this.droplab = new DropLab();
+
+ this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
+ this.getDroplabConfig());
+ }
+
+ getDroplabConfig() {
+ return {
+ InputSetter: [{
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ }, {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-text',
+ }],
+ };
+ }
+
+ bindEvents() {
+ this.createMergeRequestButton
+ .addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ }
+
+ isBusy() {
+ return this.isCreatingMergeRequest ||
+ this.mergeRequestCreated ||
+ this.isCreatingBranch ||
+ this.branchCreated;
+ }
+
+ onClickCreateMergeRequestButton(e) {
+ let xhr = null;
+ e.preventDefault();
+
+ if (this.isBusy()) {
+ return;
+ }
+
+ if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
+ xhr = this.createMergeRequest();
+ } else if (e.target.dataset.action === CREATE_BRANCH) {
+ xhr = this.createBranch();
+ }
+
+ xhr.fail(() => {
+ this.isCreatingMergeRequest = false;
+ this.isCreatingBranch = false;
+ });
+
+ xhr.always(() => this.enable());
+
+ this.disable();
+ }
+
+ createMergeRequest() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createMrPath,
+ beforeSend: () => (this.isCreatingMergeRequest = true),
+ })
+ .done((data) => {
+ this.mergeRequestCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ }
+
+ createBranch() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createBranchPath,
+ beforeSend: () => (this.isCreatingBranch = true),
+ })
+ .done((data) => {
+ this.branchCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ }
+}
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index ff10f19a4fe..ff06092e4d6 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
- // form and textarea event listeners
- this.addEventListeners();
}
+ // form and textarea event listeners
+ this.addEventListeners();
gl.text.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 011043e992f..694c6177a07 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
-/* global Flash */
+ /* global Flash */
+import CreateMergeRequestDropdown from './create_merge_request_dropdown';
require('./flash');
require('~/lib/utils/text_utility');
@@ -18,48 +19,49 @@ class Issue {
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
- Issue.initIssueBtnEventListeners();
+ this.initIssueBtnEventListeners();
}
Issue.$btnNewBranch = $('#new-branch');
+ Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
Issue.initMergeRequests();
Issue.initRelatedBranches();
- Issue.initCanCreateBranch();
+
+ if (Issue.createMrDropdownWrap) {
+ this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
+ }
}
- static initIssueBtnEventListeners() {
+ initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
-
const closeButtons = $('a.btn-close');
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
const reopenButtons = $('a.btn-reopen');
- return closeButtons.add(reopenButtons).on('click', function(e) {
- var $this, shouldSubmit, url;
+ return closeButtons.add(reopenButtons).on('click', (e) => {
+ var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
- $this = $(this);
- shouldSubmit = $this.hasClass('btn-comment');
+ $button = $(e.currentTarget);
+ shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
- Issue.submitNoteForm($this.closest('form'));
+ Issue.submitNoteForm($button.closest('form'));
}
- $this.prop('disabled', true);
- Issue.setNewBranchButtonState(true, null);
- url = $this.attr('href');
+ $button.prop('disabled', true);
+ url = $button.attr('href');
return $.ajax({
type: 'PUT',
url: url
- }).fail(function(jqXHR, textStatus, errorThrown) {
- new Flash(issueFailMessage);
- Issue.initCanCreateBranch();
- }).done(function(data, textStatus, jqXHR) {
+ })
+ .fail(() => new Flash(issueFailMessage))
+ .done((data) => {
if ('id' in data) {
$(document).trigger('issuable:change');
- const isClosed = $this.hasClass('btn-close');
+ const isClosed = $button.hasClass('btn-close');
closeButtons.toggleClass('hidden', isClosed);
reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed);
@@ -68,12 +70,21 @@ class Issue {
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+
+ if (this.createMergeRequestDropdown) {
+ if (isClosed) {
+ this.createMergeRequestDropdown.unavailable();
+ this.createMergeRequestDropdown.disable();
+ } else {
+ // We should check in case a branch was created in another tab
+ this.createMergeRequestDropdown.checkAbilityToCreateBranch();
+ }
+ }
} else {
new Flash(issueFailMessage);
}
- $this.prop('disabled', false);
- Issue.initCanCreateBranch();
+ $button.prop('disabled', false);
});
});
}
@@ -109,29 +120,6 @@ class Issue {
}
});
}
-
- static initCanCreateBranch() {
- // If the user doesn't have the required permissions the container isn't
- // rendered at all.
- if (Issue.$btnNewBranch.length === 0) {
- return;
- }
- return $.getJSON(Issue.$btnNewBranch.data('path')).fail(function() {
- Issue.setNewBranchButtonState(false, false);
- new Flash('Failed to check if a new branch can be created.');
- }).done(function(data) {
- Issue.setNewBranchButtonState(false, data.can_create_branch);
- });
- }
-
- static setNewBranchButtonState(isPending, canCreate) {
- if (Issue.$btnNewBranch.length === 0) {
- return;
- }
-
- Issue.$btnNewBranch.find('.available').toggle(!isPending && canCreate);
- Issue.$btnNewBranch.find('.unavailable').toggle(!isPending && !canCreate);
- }
}
export default Issue;
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 974fb0d83da..87f03a40eba 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -4,6 +4,7 @@
/* global ResolveService */
/* global mrRefreshWidgetUrl */
+import $ from 'jquery';
import Cookies from 'js-cookie';
import CommentTypeToggle from './comment_type_toggle';
@@ -16,6 +17,10 @@ require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
require('./task_list');
+const normalizeNewlines = function(str) {
+ return str.replace(/\r\n/g, '\n');
+};
+
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -42,13 +47,17 @@ require('./task_list');
this.refresh = bind(this.refresh, this);
this.keydownNoteText = bind(this.keydownNoteText, this);
this.toggleCommitList = bind(this.toggleCommitList, this);
+
this.notes_url = notes_url;
this.note_ids = note_ids;
+ // Used to keep track of updated notes while people are editing things
+ this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at;
this.noteable_url = document.URL;
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
+
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
@@ -128,7 +137,7 @@ require('./task_list');
$(document).off("click", ".js-discussion-reply-button");
$(document).off("click", ".js-add-diff-note-button");
$(document).off("visibilitychange");
- $(document).off("keyup", ".js-note-text");
+ $(document).off("keyup input", ".js-note-text");
$(document).off("click", ".js-note-target-reopen");
$(document).off("click", ".js-note-target-close");
$(document).off("click", ".js-note-discard");
@@ -267,20 +276,20 @@ require('./task_list');
return this.initRefresh();
};
- Notes.prototype.handleCreateChanges = function(note) {
+ Notes.prototype.handleCreateChanges = function(noteEntity) {
var votesBlock;
- if (typeof note === 'undefined') {
+ if (typeof noteEntity === 'undefined') {
return;
}
- if (note.commands_changes) {
- if ('merge' in note.commands_changes) {
+ if (noteEntity.commands_changes) {
+ if ('merge' in noteEntity.commands_changes) {
$.get(mrRefreshWidgetUrl);
}
- if ('emoji_award' in note.commands_changes) {
+ if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0);
- gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award);
+ gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
return gl.awardsHandler.scrollToAwards();
}
}
@@ -292,41 +301,76 @@ require('./task_list');
Note: for rendering inline notes use renderDiscussionNote
*/
- Notes.prototype.renderNote = function(note, $form) {
- var $notesList;
- if (note.discussion_html != null) {
- return this.renderDiscussionNote(note, $form);
+ Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) {
+ if (noteEntity.discussion_html != null) {
+ return this.renderDiscussionNote(noteEntity, $form);
}
- if (!note.valid) {
- if (note.errors.commands_only) {
- new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
+ if (!noteEntity.valid) {
+ if (noteEntity.errors.commands_only) {
+ new Flash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh();
}
return;
}
- if (this.isNewNote(note)) {
- this.note_ids.push(note.id);
+ const $note = $notesList.find(`#note_${noteEntity.id}`);
+ if (this.isNewNote(noteEntity)) {
+ this.note_ids.push(noteEntity.id);
- $notesList = window.$('ul.main-notes-list');
- Notes.animateAppendNote(note.html, $notesList);
+ const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
// Update datetime format on the recent note
- gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
+ gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
this.collapseLongCommitList();
this.taskList.init();
this.refresh();
return this.updateNotesCount(1);
}
+ // The server can send the same update multiple times so we need to make sure to only update once per actual update.
+ else if (this.isUpdatedNote(noteEntity, $note)) {
+ const isEditing = $note.hasClass('is-editing');
+ const initialContent = normalizeNewlines(
+ $note.find('.original-note-content').text().trim()
+ );
+ const $textarea = $note.find('.js-note-text');
+ const currentContent = $textarea.val();
+ // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
+ const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
+ const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote;
+
+ if (isEditing && isTextareaUntouched) {
+ $textarea.val(noteEntity.note);
+ this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
+ }
+ else if (isEditing && !isTextareaUntouched) {
+ this.putConflictEditWarningInPlace(noteEntity, $note);
+ this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
+ }
+ else {
+ const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
+
+ // Update datetime format on the recent note
+ gl.utils.localTimeAgo($updatedNote.find('.js-timeago'), false);
+ }
+ }
};
/*
Check if note does not exists on page
*/
- Notes.prototype.isNewNote = function(note) {
- return $.inArray(note.id, this.note_ids) === -1;
+ Notes.prototype.isNewNote = function(noteEntity) {
+ return $.inArray(noteEntity.id, this.note_ids) === -1;
+ };
+
+ Notes.prototype.isUpdatedNote = function(noteEntity, $note) {
+ // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
+ const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
+ const currentNoteText = normalizeNewlines(
+ $note.find('.original-note-content').text().trim()
+ );
+ return sanitizedNoteNote !== currentNoteText;
};
Notes.prototype.isParallelView = function() {
@@ -339,31 +383,31 @@ require('./task_list');
Note: for rendering inline notes use renderDiscussionNote
*/
- Notes.prototype.renderDiscussionNote = function(note, $form) {
+ Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
var discussionContainer, form, row, lineType, diffAvatarContainer;
- if (!this.isNewNote(note)) {
+ if (!this.isNewNote(noteEntity)) {
return;
}
- this.note_ids.push(note.id);
- form = $form || $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
+ this.note_ids.push(noteEntity.id);
+ form = $form || $(".js-discussion-note-form[data-discussion-id='" + noteEntity.discussion_id + "']");
row = form.closest("tr");
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`);
+ discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
if (discussionContainer.length === 0) {
- if (note.diff_discussion_html) {
- var $discussion = $(note.diff_discussion_html).renderGFM();
+ if (noteEntity.diff_discussion_html) {
+ var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
+ var $notes = $discussion.find('.notes[data-discussion-id="' + noteEntity.discussion_id + '"]');
var contentContainerClass = '.' + $notes.closest('.notes_content')
.attr('class')
.split(' ')
@@ -373,17 +417,18 @@ require('./task_list');
}
}
// Init discussion on 'Discussion' page if it is merge request page
- if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
- Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list'));
+ const page = $('body').attr('data-page');
+ if ((page && page.indexOf('projects:merge_request') === 0) || !noteEntity.diff_discussion_html) {
+ Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
}
} else {
// append new note to all matching discussions
- Notes.animateAppendNote(note.html, discussionContainer);
+ Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
- this.renderDiscussionAvatar(diffAvatarContainer, note);
+ this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
gl.utils.localTimeAgo($('.js-timeago'), false);
@@ -397,13 +442,13 @@ require('./task_list');
.get(0);
};
- Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
+ Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, noteEntity) {
var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) {
avatarHolder = document.createElement('diff-note-avatars');
- avatarHolder.setAttribute('discussion-id', note.discussion_id);
+ avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
diffAvatarContainer.append(avatarHolder);
@@ -550,16 +595,16 @@ require('./task_list');
Updates the current note field.
*/
- Notes.prototype.updateNote = function(_xhr, note, _status) {
+ Notes.prototype.updateNote = function(_xhr, noteEntity, _status) {
var $html, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
- $html = $(note.html);
+ $html = $(noteEntity.html);
this.revertNoteEditForm();
gl.utils.localTimeAgo($('.js-timeago', $html));
$html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML
- $note_li = $('.note-row-' + note.id);
+ $note_li = $('.note-row-' + noteEntity.id);
$note_li.replaceWith($html);
@@ -570,7 +615,7 @@ require('./task_list');
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
- var currentContent = $el.find('.note-textarea').val();
+ var currentContent = $el.find('.js-note-text').val();
var isAllowed = true;
if (currentContent === initialContent) {
@@ -584,7 +629,7 @@ require('./task_list');
gl.utils.scrollToElement($el);
}
- $el.find('.js-edit-warning').show();
+ $el.find('.js-finish-edit-warning').show();
isAllowed = false;
}
@@ -603,7 +648,7 @@ require('./task_list');
var $target = $(e.target);
var $editForm = $(this.getEditFormSelector($target));
var $note = $target.closest('.note');
- var $currentlyEditing = $('.note.is-editting:visible');
+ var $currentlyEditing = $('.note.is-editing:visible');
if ($currentlyEditing.length) {
var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
@@ -615,7 +660,7 @@ require('./task_list');
$note.find('.js-note-attachment-delete').show();
$editForm.addClass('current-note-edit-form');
- $note.addClass('is-editting');
+ $note.addClass('is-editing');
this.putEditFormInPlace($target);
};
@@ -627,21 +672,34 @@ require('./task_list');
Notes.prototype.cancelEdit = function(e) {
e.preventDefault();
- var $target = $(e.target);
- var note = $target.closest('.note');
- note.find('.js-edit-warning').hide();
+ const $target = $(e.target);
+ const $note = $target.closest('.note');
+ const noteId = $note.attr('data-note-id');
+
this.revertNoteEditForm($target);
- return this.removeNoteEditForm(note);
+
+ if (this.updatedNotesTrackingMap[noteId]) {
+ const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
+ $note.replaceWith($newNote);
+ this.updatedNotesTrackingMap[noteId] = null;
+
+ // Update datetime format on the recent note
+ gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
+ }
+ else {
+ $note.find('.js-finish-edit-warning').hide();
+ this.removeNoteEditForm($note);
+ }
};
Notes.prototype.revertNoteEditForm = function($target) {
- $target = $target || $('.note.is-editting:visible');
+ $target = $target || $('.note.is-editing:visible');
var selector = this.getEditFormSelector($target);
var $editForm = $(selector);
$editForm.insertBefore('.notes-form');
$editForm.find('.js-comment-button').enable();
- $editForm.find('.js-edit-warning').hide();
+ $editForm.find('.js-finish-edit-warning').hide();
};
Notes.prototype.getEditFormSelector = function($el) {
@@ -654,11 +712,11 @@ require('./task_list');
return selector;
};
- Notes.prototype.removeNoteEditForm = function(note) {
- var form = note.find('.current-note-edit-form');
- note.removeClass('is-editting');
+ Notes.prototype.removeNoteEditForm = function($note) {
+ var form = $note.find('.current-note-edit-form');
+ $note.removeClass('is-editing');
form.removeClass('current-note-edit-form');
- form.find('.js-edit-warning').hide();
+ form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text.
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
@@ -683,9 +741,9 @@ require('./task_list');
// to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
// where $("#noteId") would return only one.
return function(i, el) {
- var note, notes;
- note = $(el);
- notes = note.closest(".discussion-notes");
+ var $note, $notes;
+ $note = $(el);
+ $notes = $note.closest(".discussion-notes");
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
@@ -693,18 +751,18 @@ require('./task_list');
}
}
- note.remove();
+ $note.remove();
// check if this is the last note for this line
- if (notes.find(".note").length === 0) {
- var notesTr = notes.closest("tr");
+ if ($notes.find(".note").length === 0) {
+ var notesTr = $notes.closest("tr");
// "Discussions" tab
- notes.closest(".timeline-entry").remove();
+ $notes.closest(".timeline-entry").remove();
// The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) {
- notes.remove();
+ $notes.remove();
} else {
notesTr.remove();
}
@@ -723,12 +781,11 @@ require('./task_list');
*/
Notes.prototype.removeAttachment = function() {
- var note;
- note = $(this).closest(".note");
- note.find(".note-attachment").remove();
- note.find(".note-body > .note-text").show();
- note.find(".note-header").show();
- return note.find(".current-note-edit-form").remove();
+ const $note = $(this).closest(".note");
+ $note.find(".note-attachment").remove();
+ $note.find(".note-body > .note-text").show();
+ $note.find(".note-header").show();
+ return $note.find(".current-note-edit-form").remove();
};
/*
@@ -1004,6 +1061,19 @@ require('./task_list');
$editForm.find('.referenced-users').hide();
};
+ Notes.prototype.putConflictEditWarningInPlace = function(noteEntity, $note) {
+ if ($note.find('.js-conflict-edit-warning').length === 0) {
+ const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
+ This comment has changed since you started editing, please review the
+ <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
+ updated comment
+ </a>
+ to ensure information is not lost
+ </div>`);
+ $alert.insertAfter($note.find('.note-text'));
+ }
+ };
+
Notes.prototype.updateNotesCount = function(updateCount) {
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
};
@@ -1064,11 +1134,20 @@ require('./task_list');
return $form;
};
- Notes.animateAppendNote = function(noteHTML, $notesList) {
- const $note = window.$(noteHTML);
+ Notes.animateAppendNote = function(noteHtml, $notesList) {
+ const $note = $(noteHtml);
$note.addClass('fade-in').renderGFM();
$notesList.append($note);
+ return $note;
+ };
+
+ Notes.animateUpdateNote = function(noteHtml, $note) {
+ const $updatedNote = $(noteHtml);
+
+ $updatedNote.addClass('fade-in').renderGFM();
+ $note.replaceWith($updatedNote);
+ return $updatedNote;
};
return Notes;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1b4694377b3..feefaad8a15 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -425,12 +425,6 @@
float: right;
}
-.diffs {
- .content-block {
- border-bottom: none;
- }
-}
-
.files-changed {
border-bottom: none;
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 2aa52986e0a..b18bbc329c3 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -161,3 +161,86 @@ ul.related-merge-requests > li {
.recaptcha {
margin-bottom: 30px;
}
+
+.new-branch-col {
+ padding-top: 10px;
+}
+
+.create-mr-dropdown-wrap {
+ .btn-group:not(.hide) {
+ display: flex;
+ }
+
+ .js-create-merge-request {
+ flex-grow: 1;
+ flex-shrink: 0;
+ }
+
+ .dropdown-menu {
+ width: 300px;
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ display: none;
+ }
+
+ .dropdown-toggle {
+ .fa-caret-down {
+ pointer-events: none;
+ margin-left: 0;
+ color: inherit;
+ margin-left: 0;
+ }
+ }
+
+ li:not(.divider) {
+ padding: 6px;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ }
+
+ &.droplab-item-selected {
+ .icon-container {
+ i {
+ visibility: visible;
+ }
+ }
+ }
+
+ .icon-container {
+ float: left;
+ padding-left: 6px;
+
+ i {
+ visibility: hidden;
+ }
+ }
+
+ .description {
+ padding-left: 30px;
+ font-size: 13px;
+
+ strong {
+ display: block;
+ font-weight: 600;
+ }
+ }
+ }
+}
+
+@media (min-width: $screen-sm-min) {
+ .new-branch-col {
+ padding-top: 0;
+ text-align: right;
+ }
+
+ .create-mr-dropdown-wrap {
+ .btn-group:not(.hide) {
+ display: inline-block;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index be7193bae04..8dbac76e30a 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -133,3 +133,55 @@
right: 160px;
}
}
+
+.flex-project-members-panel {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ @media (max-width: $screen-sm-min) {
+ display: block;
+
+ .flex-project-title {
+ vertical-align: top;
+ display: inline-block;
+ max-width: 90%;
+ }
+ }
+
+ .flex-project-title {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .badge {
+ height: 17px;
+ line-height: 16px;
+ margin-right: 5px;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ }
+
+ .flex-project-members-form {
+ flex-wrap: nowrap;
+ white-space: nowrap;
+ margin-left: auto;
+ }
+}
+
+.panel {
+ .panel-heading {
+ .badge {
+ margin-top: 0;
+ }
+
+ @media (max-width: $screen-sm-min) {
+ .badge {
+ margin-right: 0;
+ margin-left: 0;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 6a419384a34..bca62b7fc31 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -511,7 +511,6 @@
.mr-version-controls {
background: $gray-light;
- border-bottom: 1px solid $border-color;
color: $gl-text-color;
.mr-version-menus-container {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 7cf74502a3a..f89150ebead 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -67,7 +67,7 @@ ul.notes {
}
}
- &.is-editting {
+ &.is-editing {
.note-header,
.note-text,
.edited-text {
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 59222637961..a13588b4218 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -16,7 +16,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def browse
- directory = params[:path] ? "#{params[:path]}/" : ''
+ @path = params[:path]
+ directory = @path ? "#{@path}/" : ''
@entry = build.artifacts_metadata_entry(directory)
render_404 unless @entry.exists?
@@ -60,7 +61,10 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def build
- @build ||= build_from_id || build_from_ref
+ @build ||= begin
+ build = build_from_id || build_from_ref
+ build&.present(current_user: current_user)
+ end
end
def build_from_id
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 840405f38cb..f0f031303d8 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -46,20 +46,28 @@ class Projects::BranchesController < Projects::ApplicationController
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
- if result[:status] == :success
- @branch = result[:branch]
-
- if redirect_to_autodeploy
- redirect_to(
- url_to_autodeploy_setup(project, branch_name),
- notice: view_context.autodeploy_flash_notice(branch_name))
- else
- redirect_to namespace_project_tree_path(@project.namespace, @project,
- @branch.name)
+ respond_to do |format|
+ format.html do
+ if result[:status] == :success
+ if redirect_to_autodeploy
+ redirect_to url_to_autodeploy_setup(project, branch_name),
+ notice: view_context.autodeploy_flash_notice(branch_name)
+ else
+ redirect_to namespace_project_tree_path(@project.namespace, @project, branch_name)
+ end
+ else
+ @error = result[:message]
+ render action: 'new'
+ end
+ end
+
+ format.json do
+ if result[:status] == :success
+ render json: { name: branch_name, url: namespace_project_tree_url(@project.namespace, @project, branch_name) }
+ else
+ render json: result[:messsage], status: :unprocessable_entity
+ end
end
- else
- @error = result[:message]
- render action: 'new'
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cbf67137261..af9157bfbb5 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
- :related_branches, :can_create_branch, :rendered_title]
+ :related_branches, :can_create_branch, :rendered_title, :create_merge_request]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show, :rendered_title]
@@ -22,6 +22,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update]
+ # Allow create a new branch and empty WIP merge request from current issue
+ before_action :authorize_create_merge_request!, only: [:create_merge_request]
+
respond_to :html
def index
@@ -191,7 +194,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.json do
- render json: { can_create_branch: can_create }
+ render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? }
end
end
end
@@ -201,6 +204,16 @@ class Projects::IssuesController < Projects::ApplicationController
render json: { title: view_context.markdown_field(@issue, :title) }
end
+ def create_merge_request
+ result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
+
+ if result[:status] == :success
+ render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
+ else
+ render json: result[:messsage], status: :unprocessable_entity
+ end
+ end
+
protected
def issue
@@ -224,6 +237,10 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_issue, @project)
end
+ def authorize_create_merge_request!
+ return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ end
+
def module_enabled
return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 09dc8b38229..a63b7ff0bed 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -120,7 +120,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
define_diff_comment_vars
else
build_merge_request
- @diffs = @merge_request.diffs(diff_options)
+ @compare = @merge_request
+ @diffs = @compare.diffs(diff_options)
@diff_notes_disabled = true
end
@@ -584,12 +585,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
- @diffs =
+ @compare =
if @start_sha
- @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
+ @merge_request_diff.compare_with(@start_sha)
else
- @merge_request_diff.diffs(diff_options)
+ @merge_request_diff
end
+
+ @diffs = @compare.diffs(diff_options)
end
def define_diff_comment_vars
@@ -598,11 +601,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
noteable_id: @merge_request.id
}
- @diff_notes_disabled = !@merge_request_diff.latest? || @start_sha
+ @diff_notes_disabled = false
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
- @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@merge_request_diff.diff_refs)
+ @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index a0b08ad130f..a02cc477e08 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob?
- if @blob.valid_lfs_pointer?
+ if @blob.stored_externally?
send_lfs_object
else
send_git_blob @repository, @blob
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 377b080b3c6..37b6f4ad5cc 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
- elsif blob.valid_lfs_pointer?
+ elsif blob.stored_externally?
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
@@ -95,7 +95,7 @@ module BlobHelper
end
def can_modify_blob?(blob, project = @project, ref = @ref)
- !blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
+ !blob.stored_externally? && can_edit_tree?(project, ref)
end
def leave_edit_message
@@ -223,7 +223,9 @@ module BlobHelper
end
def open_raw_blob_button(blob)
- if blob.raw_binary?
+ return if blob.empty?
+
+ if blob.raw_binary? || blob.stored_externally?
icon = icon('download')
title = 'Download'
else
@@ -244,19 +246,27 @@ module BlobHelper
viewer.max_size
end
"it is larger than #{number_to_human_size(max_size)}"
- when :server_side_but_stored_in_lfs
- "it is stored in LFS"
+ when :server_side_but_stored_externally
+ case viewer.blob.external_storage
+ when :lfs
+ 'it is stored in LFS'
+ else
+ 'it is stored externally'
+ end
end
end
def blob_render_error_options(viewer)
+ error = viewer.render_error
options = []
- if viewer.render_error == :too_large && viewer.can_override_max_size?
+ if error == :too_large && viewer.can_override_max_size?
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
end
- if viewer.rich? && viewer.blob.rendered_as_text?
+ # If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
+ # so don't bother switching.
+ if viewer.rich? && viewer.blob.rendered_as_text? && error != :server_side_but_stored_externally
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index eab0738a368..08180883eb9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -60,20 +60,16 @@ module NotesHelper
note.project.team.human_max_access(note.author_id)
end
- def discussion_diff_path(discussion)
- if discussion.for_merge_request? && discussion.diff_discussion?
- if discussion.active?
- # Without a diff ID, the link always points to the latest diff version
- diff_id = nil
- elsif merge_request_diff = discussion.latest_merge_request_diff
- diff_id = merge_request_diff.id
- else
- # If the discussion is not active, and we cannot find the latest
- # merge request diff for this discussion, we return no path at all.
- return
- end
-
- diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, diff_id: diff_id, anchor: discussion.line_code)
+ def discussion_path(discussion)
+ if discussion.for_merge_request?
+ return unless discussion.diff_discussion?
+
+ version_params = discussion.merge_request_version_params
+ return unless version_params
+
+ path_params = version_params.merge(anchor: discussion.line_code)
+
+ diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, path_params)
elsif discussion.for_commit?
anchor = discussion.line_code if discussion.diff_discussion?
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index f7b5a5f4dfc..a91e3da309c 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -76,7 +76,7 @@ module TreeHelper
"A new branch will be created in your fork and a new merge request will be started."
end
- def tree_breadcrumbs(tree, max_links = 2)
+ def path_breadcrumbs(max_links = 6)
if @path.present?
part_path = ""
parts = @path.split('/')
@@ -88,7 +88,7 @@ module TreeHelper
part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part)
- yield(part, tree_join(@ref, part_path))
+ yield(part, part_path)
end
end
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 1cdb8811cff..a4fae22a0c4 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -28,7 +28,7 @@ class Blob < SimpleDelegator
BlobViewer::Sketch,
BlobViewer::Video,
-
+
BlobViewer::PDF,
BlobViewer::BinarySTL,
@@ -75,19 +75,37 @@ class Blob < SimpleDelegator
end
def no_highlighting?
- size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
+ raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
+ end
+
+ def empty?
+ raw_size == 0
end
def too_large?
size && truncated?
end
+ def external_storage_error?
+ if external_storage == :lfs
+ !project&.lfs_enabled?
+ else
+ false
+ end
+ end
+
+ def stored_externally?
+ return @stored_externally if defined?(@stored_externally)
+
+ @stored_externally = external_storage && !external_storage_error?
+ end
+
# Returns the size of the file that this blob represents. If this blob is an
# LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
# the size of the blob itself.
def raw_size
- if valid_lfs_pointer?
- lfs_size
+ if stored_externally?
+ external_size
else
size
end
@@ -98,9 +116,13 @@ class Blob < SimpleDelegator
# text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself.
def raw_binary?
- if valid_lfs_pointer?
+ if stored_externally?
if rich_viewer
rich_viewer.binary?
+ elsif Linguist::Language.find_by_filename(name).any?
+ false
+ elsif _mime_type
+ _mime_type.binary?
else
true
end
@@ -118,15 +140,7 @@ class Blob < SimpleDelegator
end
def readable_text?
- text? && !valid_lfs_pointer? && !too_large?
- end
-
- def valid_lfs_pointer?
- lfs_pointer? && project&.lfs_enabled?
- end
-
- def invalid_lfs_pointer?
- lfs_pointer? && !project&.lfs_enabled?
+ text? && !stored_externally? && !too_large?
end
def simple_viewer
@@ -165,10 +179,10 @@ class Blob < SimpleDelegator
end
def rich_viewer_class
- return if invalid_lfs_pointer? || empty?
+ return if empty? || external_storage_error?
classes =
- if valid_lfs_pointer?
+ if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS
elsif binary?
BINARY_VIEWERS
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index f944b00c9d3..a8b91d8d6bc 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -70,12 +70,13 @@ module BlobViewer
return @render_error if defined?(@render_error)
@render_error =
- if server_side_but_stored_in_lfs?
- # Files stored in LFS can only be rendered using a client-side viewer,
+ if server_side_but_stored_externally?
+ # Files that are not stored in the repository, like LFS files and
+ # build artifacts, can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from
# `blob_raw_url` using AJAX.
- :server_side_but_stored_in_lfs
+ :server_side_but_stored_externally
elsif override_max_size ? absolutely_too_large? : too_large?
:too_large
end
@@ -89,8 +90,8 @@ module BlobViewer
private
- def server_side_but_stored_in_lfs?
- server_side? && blob.valid_lfs_pointer?
+ def server_side_but_stored_externally?
+ server_side? && blob.stored_externally?
end
end
end
diff --git a/app/models/concerns/blob_like.rb b/app/models/concerns/blob_like.rb
new file mode 100644
index 00000000000..adb81561000
--- /dev/null
+++ b/app/models/concerns/blob_like.rb
@@ -0,0 +1,48 @@
+module BlobLike
+ extend ActiveSupport::Concern
+ include Linguist::BlobHelper
+
+ def id
+ raise NotImplementedError
+ end
+
+ def name
+ raise NotImplementedError
+ end
+
+ def path
+ raise NotImplementedError
+ end
+
+ def size
+ 0
+ end
+
+ def data
+ nil
+ end
+
+ def mode
+ nil
+ end
+
+ def binary?
+ false
+ end
+
+ def load_all_data!(repository)
+ # No-op
+ end
+
+ def truncated?
+ false
+ end
+
+ def external_storage
+ nil
+ end
+
+ def external_size
+ nil
+ end
+end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 8ee42875670..a7bdf5587b2 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -11,6 +11,7 @@ module DiscussionOnDiff
:diff_line,
:for_line?,
:active?,
+ :created_at_diff?,
to: :first_note
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 6c27dd5aa5c..6359f7596b1 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -30,6 +30,10 @@ module NoteOnDiff
raise NotImplementedError
end
+ def created_at_diff?(diff_refs)
+ false
+ end
+
private
def noteable_diff_refs
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 6a6466b493b..d627fbe327f 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -10,7 +10,6 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
- :latest_merge_request_diff,
to: :first_note
@@ -18,6 +17,25 @@ class DiffDiscussion < Discussion
false
end
+ def merge_request_version_params
+ return unless for_merge_request?
+
+ if active?
+ {}
+ else
+ diff_refs = position.diff_refs
+
+ if diff = noteable.merge_request_diff_for(diff_refs)
+ { diff_id: diff.id }
+ elsif diff = noteable.merge_request_diff_for(diff_refs.head_sha)
+ {
+ diff_id: diff.id,
+ start_sha: diff_refs.start_sha
+ }
+ end
+ end
+ end
+
def reply_attributes
super.merge(
original_position: original_position.to_json,
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index abe4518d62a..76c59199afd 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -65,10 +65,11 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
- def latest_merge_request_diff
- return unless for_merge_request?
+ def created_at_diff?(diff_refs)
+ return false unless supported?
+ return true if for_commit?
- self.noteable.merge_request_diff_for(self.position.diff_refs)
+ self.original_position.diff_refs == diff_refs
end
private
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 305fc01f041..78bde6820da 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -143,6 +143,14 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
+ # Returns boolean if a related branch exists for the current issue
+ # ignores merge requests branchs
+ def has_related_branch?
+ project.repository.branch_names.any? do |branch|
+ /\A#{iid}-(?!\d+-stable)/i =~ branch
+ end
+ end
+
# To allow polymorphism with MergeRequest.
def source_project
project
diff --git a/app/models/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb
index e617ce36f56..3c1d34db5fa 100644
--- a/app/models/legacy_diff_discussion.rb
+++ b/app/models/legacy_diff_discussion.rb
@@ -9,14 +9,14 @@ class LegacyDiffDiscussion < Discussion
memoized_values << :active
- def legacy_diff_discussion?
- true
- end
-
def self.note_class
LegacyDiffNote
end
+ def legacy_diff_discussion?
+ true
+ end
+
def active?(*args)
return @active if @active.present?
@@ -27,6 +27,16 @@ class LegacyDiffDiscussion < Discussion
!active?
end
+ def merge_request_version_params
+ return unless for_merge_request?
+
+ if active?
+ {}
+ else
+ nil
+ end
+ end
+
def reply_attributes
super.merge(line_code: line_code)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 365fa4f1e70..12c5481cd6d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -374,12 +374,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true)
end
- def merge_request_diff_for(diff_refs)
- @merge_request_diffs_by_diff_refs ||= Hash.new do |h, diff_refs|
- h[diff_refs] = merge_request_diffs.viewable.select_without_diff.find_by_diff_refs(diff_refs)
- end
-
- @merge_request_diffs_by_diff_refs[diff_refs]
+ def merge_request_diff_for(diff_refs_or_sha)
+ @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
+ diffs = merge_request_diffs.viewable.select_without_diff
+ h[diff_refs_or_sha] =
+ if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
+ diffs.find_by_diff_refs(diff_refs_or_sha)
+ else
+ diffs.find_by(head_commit_sha: diff_refs_or_sha)
+ end
+ end
+
+ @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
end
def reload_diff_if_branch_changed
diff --git a/app/models/note.rb b/app/models/note.rb
index e720bfba030..b06985b4a6f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -115,11 +115,19 @@ class Note < ActiveRecord::Base
end
def grouped_diff_discussions(diff_refs = nil)
- diff_notes.
- fresh.
- discussions.
- select { |n| n.active?(diff_refs) }.
- group_by(&:line_code)
+ groups = {}
+
+ diff_notes.fresh.discussions.each do |discussion|
+ if discussion.active?(diff_refs)
+ discussions = groups[discussion.line_code] ||= []
+ elsif diff_refs && discussion.created_at_diff?(diff_refs)
+ discussions = groups[discussion.original_line_code] ||= []
+ end
+
+ discussions << discussion if discussions
+ end
+
+ groups
end
def count_for_collection(ids, type)
@@ -141,10 +149,6 @@ class Note < ActiveRecord::Base
true
end
- def latest_merge_request_diff
- nil
- end
-
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index 7621a5fa2d8..e2ad586aea7 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -50,5 +50,16 @@ module ChatMessage
def link(text, url)
"[#{text}](#{url})"
end
+
+ def pretty_duration(seconds)
+ parse_string =
+ if duration < 1.hour
+ '%M:%S'
+ else
+ '%H:%M:%S'
+ end
+
+ Time.at(seconds).utc.strftime(parse_string)
+ end
end
end
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 4628d9b1a7b..47b68f00cff 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -15,7 +15,7 @@ module ChatMessage
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
- @duration = pipeline_attributes[:duration]
+ @duration = pipeline_attributes[:duration].to_i
@pipeline_id = pipeline_attributes[:id]
end
@@ -37,7 +37,7 @@ module ChatMessage
{
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}",
- text: "in #{duration} #{time_measure}",
+ text: "in #{pretty_duration(duration)}",
image: user_avatar || ''
}
end
@@ -45,7 +45,7 @@ module ChatMessage
private
def message
- "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{time_measure}"
+ "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
end
def humanized_status
@@ -84,9 +84,5 @@ module ChatMessage
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
end
-
- def time_measure
- 'second'.pluralize(duration)
- end
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index ba34d570dbd..0c797dd5814 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -789,7 +789,7 @@ class Repository
}
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
- Rugged::Commit.create(rugged, options)
+ create_commit(options)
end
end
# rubocop:enable Metrics/ParameterLists
@@ -836,7 +836,7 @@ class Repository
tree: merge_index.write_tree(rugged),
)
- commit_id = Rugged::Commit.create(rugged, actual_options)
+ commit_id = create_commit(actual_options)
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
@@ -859,12 +859,11 @@ class Repository
committer = user_to_committer(user)
- Rugged::Commit.create(rugged,
- message: commit.revert_message(user),
- author: committer,
- committer: committer,
- tree: revert_tree_id,
- parents: [start_commit.sha])
+ create_commit(message: commit.revert_message(user),
+ author: committer,
+ committer: committer,
+ tree: revert_tree_id,
+ parents: [start_commit.sha])
end
end
@@ -883,16 +882,15 @@ class Repository
committer = user_to_committer(user)
- Rugged::Commit.create(rugged,
- message: commit.message,
- author: {
- email: commit.author_email,
- name: commit.author_name,
- time: commit.authored_date
- },
- committer: committer,
- tree: cherry_pick_tree_id,
- parents: [start_commit.sha])
+ create_commit(message: commit.message,
+ author: {
+ email: commit.author_email,
+ name: commit.author_name,
+ time: commit.authored_date
+ },
+ committer: committer,
+ tree: cherry_pick_tree_id,
+ parents: [start_commit.sha])
end
end
@@ -900,7 +898,7 @@ class Repository
GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user)
- Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
+ create_commit(params.merge(author: committer, committer: committer))
end
end
@@ -1142,6 +1140,12 @@ class Repository
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end
+ def create_commit(params = {})
+ params[:message].delete!("\r")
+
+ Rugged::Commit.create(rugged, params)
+ end
+
def repository_storage_path
@project.repository_storage_path
end
diff --git a/app/models/snippet_blob.rb b/app/models/snippet_blob.rb
index d6cab74eb1a..fa5fa151607 100644
--- a/app/models/snippet_blob.rb
+++ b/app/models/snippet_blob.rb
@@ -1,5 +1,5 @@
class SnippetBlob
- include Linguist::BlobHelper
+ include BlobLike
attr_reader :snippet
@@ -28,32 +28,4 @@ class SnippetBlob
Banzai.render_field(snippet, :content)
end
-
- def mode
- nil
- end
-
- def binary?
- false
- end
-
- def load_all_data!(repository)
- # No-op
- end
-
- def lfs_pointer?
- false
- end
-
- def lfs_oid
- nil
- end
-
- def lfs_size
- nil
- end
-
- def truncated?
- false
- end
end
diff --git a/app/serializers/merge_request_create_entity.rb b/app/serializers/merge_request_create_entity.rb
new file mode 100644
index 00000000000..11234313293
--- /dev/null
+++ b/app/serializers/merge_request_create_entity.rb
@@ -0,0 +1,7 @@
+class MergeRequestCreateEntity < Grape::Entity
+ expose :iid
+
+ expose :url do |merge_request|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+end
diff --git a/app/serializers/merge_request_create_serializer.rb b/app/serializers/merge_request_create_serializer.rb
new file mode 100644
index 00000000000..08daf473319
--- /dev/null
+++ b/app/serializers/merge_request_create_serializer.rb
@@ -0,0 +1,3 @@
+class MergeRequestCreateSerializer < BaseSerializer
+ entity MergeRequestCreateEntity
+end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
new file mode 100644
index 00000000000..738cedbaed7
--- /dev/null
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -0,0 +1,54 @@
+module MergeRequests
+ class CreateFromIssueService < MergeRequests::CreateService
+ def execute
+ return error('Invalid issue iid') unless issue_iid.present? && issue.present?
+
+ result = CreateBranchService.new(project, current_user).execute(branch_name, ref)
+ return result if result[:status] == :error
+
+ SystemNoteService.new_issue_branch(issue, project, current_user, branch_name)
+
+ new_merge_request = create(merge_request)
+
+ if new_merge_request.valid?
+ success(new_merge_request)
+ else
+ error(new_merge_request.errors)
+ end
+ end
+
+ private
+
+ def issue_iid
+ @isssue_iid ||= params.delete(:issue_iid)
+ end
+
+ def issue
+ @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
+ end
+
+ def branch_name
+ @branch_name ||= issue.to_branch_name
+ end
+
+ def ref
+ project.default_branch || 'master'
+ end
+
+ def merge_request
+ MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ end
+
+ def merge_request_params
+ {
+ source_project_id: project.id,
+ source_branch: branch_name,
+ target_project_id: project.id
+ }
+ end
+
+ def success(merge_request)
+ super().merge(merge_request: merge_request)
+ end
+ end
+end
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 549364761e6..78c5b0c1dda 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -3,7 +3,7 @@
.diff-file.file-holder
.js-file-title.file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion)
.diff-content.code.js-syntax-highlight
%table
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 8440fb3d785..38e85168f40 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -20,7 +20,7 @@
= discussion.author.to_reference
started a discussion
- - url = discussion_diff_path(discussion)
+ - url = discussion_path(discussion)
- if discussion.for_commit? && @noteable != discussion.noteable
on
- commit = discussion.noteable
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 8ab9747efc5..cdcac7e4264 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -38,7 +38,7 @@
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index 9e49c93388a..34d5c3b7285 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -3,6 +3,6 @@
%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
- %span.str-truncated
- = link_to directory.name, path_to_directory
+ = link_to path_to_directory do
+ %span.str-truncated= directory.name
%td
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index de8c173f26f..9fbb30f7c7c 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,13 +1,23 @@
-- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
+- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
+= render "projects/pipelines/head"
-.top-block.row-content-block.clearfix
- .pull-right
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
- rel: 'nofollow', download: '', class: 'btn btn-default download' do
- = icon('download')
- Download artifacts archive
+= render "projects/builds/header", show_controls: false
.tree-holder
+ .nav-block
+ .tree-controls
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
+ rel: 'nofollow', download: '', class: 'btn btn-default download' do
+ = icon('download')
+ Download artifacts archive
+
+ %ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
+ - path_breadcrumbs do |title, path|
+ %li
+ = link_to truncate(title, length: 40), browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path)
+
.tree-content-holder
%table.table.tree-table
%thead
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 3f12d64d044..f04df441ccb 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -6,17 +6,14 @@
%li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
- - tree_breadcrumbs(@tree, 6) do |title, path|
+ - path_breadcrumbs do |title, path|
+ - title = truncate(title, length: 40)
%li
- - if path
- - if path.end_with?(@path)
- = link_to namespace_project_blob_path(@project.namespace, @project, path) do
- %strong
- = truncate(title, length: 40)
- - else
- = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
+ - if path == @path
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do
+ %strong= title
- else
- = link_to title, '#'
+ = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
@@ -25,5 +22,4 @@
#blob-content-holder.blob-content-holder
%article.file-holder
= render "projects/blob/header", blob: blob
-
= render 'projects/blob/content', blob: blob
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 104db85809c..a0f8f105d9a 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,10 +1,13 @@
+- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline
.content-block.build-header.top-area
.header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
- Job
- %strong.js-build-id ##{@build.id}
+ %strong
+ Job
+ = link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do
+ \##{@build.id}
in pipeline
= link_to pipeline_path(pipeline) do
%strong ##{pipeline.id}
@@ -15,13 +18,16 @@
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
%code
= @build.ref
- - if @build.user
- = render "user"
+
+ = render "projects/builds/user" if @build.user
+
= time_ago_with_tooltip(@build.created_at)
- .nav-controls
- - if can?(current_user, :create_issue, @project) && @build.failed?
- = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
- %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
- = icon('angle-double-left')
+
+ - if show_controls
+ .nav-controls
+ - if can?(current_user, :create_issue, @project) && @build.failed?
+ = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
+ %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 3e426ee9e7d..7439b8a66f7 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -35,6 +35,6 @@
- else
= diff_line_content(line.text)
-- if line_discussions
+- if line_discussions&.any?
- discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?))
= render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 13e2150f997..6bc6bf76e18 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,9 +1,29 @@
- if can?(current_user, :push_code, @project)
- .pull-right
- #new-branch.new-branch{ 'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue) }
- = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
- method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
- New branch
- = link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do
- = icon('exclamation-triangle')
- New branch unavailable
+ .create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue), create_mr_path: create_merge_request_namespace_project_issue_path(@project.namespace, @project, @issue), create_branch_path: namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
+ .btn-group.unavailable
+ %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
+ = icon('spinner', class: 'fa-spin')
+ %span.text
+ Checking branch availability…
+ .btn-group.available.hide
+ %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: 'Create a merge request', data: { action: 'create-mr' } }
+ %button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
+ = icon('caret-down')
+ %ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
+ %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
+ .menu-item
+ .icon-container
+ = icon('check')
+ .description
+ %strong Create a merge request
+ %span
+ Creates a branch named after this issue and a merge request. The source branch is '#{@project.default_branch}' by default.
+ %li.divider.droplab-item-ignore
+ %li{ role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
+ .menu-item
+ .icon-container
+ = icon('check')
+ .description
+ %strong Create a branch
+ %span
+ Creates a branch named after this issue. The source branch is '#{@project.default_branch}' by default.
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2a871966aa8..1418ad73553 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -70,8 +70,11 @@
// This element is filled in using JavaScript.
.content-block.content-block-small
- = render 'new_branch' unless @issue.confidential?
- = render 'award_emoji/awards_block', awardable: @issue, inline: true
+ .row
+ .col-sm-6
+ = render 'award_emoji/awards_block', awardable: @issue, inline: true
+ .col-sm-6.new-branch-col
+ = render 'new_branch' unless @issue.confidential?
%section.issuable-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 547be78992e..11b0c55be0b 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -35,7 +35,7 @@
%span.dropdown.inline.mr-version-compare-dropdown
%a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
%span
- - if @start_sha
+ - if @start_version
version #{version_index(@start_version)}
- else
#{@merge_request.target_branch}
@@ -59,7 +59,7 @@
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_version) do
%strong
#{@merge_request.target_branch} (base)
.monospace= short_sha(@merge_request_diff.base_commit_sha)
@@ -75,13 +75,15 @@
= succeed '.' do
%code= @merge_request.target_branch
- - if @diff_notes_disabled
+ - if @start_version || !@merge_request_diff.latest?
.comments-disabled-notif.content-block
= icon('info-circle')
- - if @start_sha
- Comments are disabled because you're comparing two versions of this merge request.
+ Not all comments are displayed because you're
+ - if @start_version
+ comparing two versions
- else
- Discussions on this version of the merge request are displayed but comment creation is disabled.
+ viewing an old version
+ of this merge request.
.pull-right
= link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 8b4e5928e0d..a1efc0b051a 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -7,7 +7,7 @@
= render 'projects/notes/hints'
.note-form-actions.clearfix
- .settings-message.note-edit-warning.js-edit-warning
+ .settings-message.note-edit-warning.js-finish-edit-warning
Finish editing this message first!
= submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index bc57f7f1c46..b0dac9de1c6 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -4,13 +4,13 @@
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- if project_nav_tab? :pipelines
- = nav_link(path: 'pipelines#index', controller: :pipelines) do
+ = nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
- if project_nav_tab? :builds
- = nav_link(controller: :builds) do
+ = nav_link(controller: [:builds, :artifacts]) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml
index ab0771b5751..f83521052ed 100644
--- a/app/views/projects/project_members/_index.html.haml
+++ b/app/views/projects/project_members/_index.html.haml
@@ -6,6 +6,12 @@
%p
Add a new member to
%strong= @project.name
+ - else
+ %p
+ Members can be added by project
+ %i Masters
+ or
+ %i Owners
.col-lg-9
.light.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 81d57c77edf..7b1a26043e1 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,9 +1,11 @@
.panel.panel-default
- .panel-heading
- Members with access to
- %strong= @project.name
+ .panel-heading.flex-project-members-panel
+ %span.flex-project-title
+ Members of
+ %strong
+ #{@project.name}
%span.badge= @project_members.total_count
- = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml
index 6e187b54a59..af9a080f0a2 100644
--- a/app/views/projects/protected_tags/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml
@@ -9,7 +9,7 @@
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Tag:
- .col-md-10
+ .col-md-10.protected-tags-dropdown
= render partial: "projects/protected_tags/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index e7b3fe3ffda..396d1ecd77b 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -9,12 +9,9 @@
%li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
- - tree_breadcrumbs(tree, 6) do |title, path|
+ - path_breadcrumbs do |title, path|
%li
- - if path
- = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- - else
- = link_to title, '#'
+ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
- if current_user
%li
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 731270d4127..9657b4eea82 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -2,7 +2,11 @@
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
-%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
+%li.timeline-entry{ id: dom_id(note),
+ class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
+ data: { author_id: note.author.id,
+ editable: note_editable,
+ note_id: note.id } }
.timeline-entry-inner
.timeline-icon
- if note.system
diff --git a/changelogs/unreleased/26883-members-page-layout-looks-broken.yml b/changelogs/unreleased/26883-members-page-layout-looks-broken.yml
new file mode 100644
index 00000000000..e0e3a529c3e
--- /dev/null
+++ b/changelogs/unreleased/26883-members-page-layout-looks-broken.yml
@@ -0,0 +1,4 @@
+---
+title: Improved UX on project members settings view
+merge_request:
+author:
diff --git a/changelogs/unreleased/28558-create-new-branch-from-issue-page.yml b/changelogs/unreleased/28558-create-new-branch-from-issue-page.yml
new file mode 100644
index 00000000000..e43b043d6c5
--- /dev/null
+++ b/changelogs/unreleased/28558-create-new-branch-from-issue-page.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to create new branch and empty WIP merge request from issue page
+merge_request:
+author:
diff --git a/changelogs/unreleased/30458-real-time-note-edits.yml b/changelogs/unreleased/30458-real-time-note-edits.yml
new file mode 100644
index 00000000000..f67348c5302
--- /dev/null
+++ b/changelogs/unreleased/30458-real-time-note-edits.yml
@@ -0,0 +1,4 @@
+---
+title: Update note edits in real-time
+merge_request:
+author:
diff --git a/changelogs/unreleased/31544-size-of-project-from-api.yml b/changelogs/unreleased/31544-size-of-project-from-api.yml
new file mode 100644
index 00000000000..a707d49aecd
--- /dev/null
+++ b/changelogs/unreleased/31544-size-of-project-from-api.yml
@@ -0,0 +1,4 @@
+---
+title: Expose project statistics on single requests via the API
+merge_request:
+author:
diff --git a/changelogs/unreleased/31647-fix-snippet-content_html.yml b/changelogs/unreleased/31647-fix-snippet-content_html.yml
new file mode 100644
index 00000000000..db6d45926fd
--- /dev/null
+++ b/changelogs/unreleased/31647-fix-snippet-content_html.yml
@@ -0,0 +1,4 @@
+---
+title: Fix caching large snippet HTML content on MySQL databases
+merge_request: 11024
+author:
diff --git a/changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml b/changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml
new file mode 100644
index 00000000000..c33fa944a83
--- /dev/null
+++ b/changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml
@@ -0,0 +1,4 @@
+---
+title: Remove carriage returns from commit messages
+merge_request: 11077
+author:
diff --git a/changelogs/unreleased/dm-artifact-browser-header.yml b/changelogs/unreleased/dm-artifact-browser-header.yml
new file mode 100644
index 00000000000..b88ab2ac7e5
--- /dev/null
+++ b/changelogs/unreleased/dm-artifact-browser-header.yml
@@ -0,0 +1,4 @@
+---
+title: Add breadcrumb, build header and pipelines submenu to artifacts browser
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-comment-on-diff-versions.yml b/changelogs/unreleased/dm-comment-on-diff-versions.yml
new file mode 100644
index 00000000000..af299713ad3
--- /dev/null
+++ b/changelogs/unreleased/dm-comment-on-diff-versions.yml
@@ -0,0 +1,4 @@
+---
+title: Allow commenting on older versions of the diff and comparisons between diff versions
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-chat-message-pretty-time.yml b/changelogs/unreleased/zj-chat-message-pretty-time.yml
new file mode 100644
index 00000000000..68bc647bab2
--- /dev/null
+++ b/changelogs/unreleased/zj-chat-message-pretty-time.yml
@@ -0,0 +1,4 @@
+---
+title: Pipeline chat notifications convert seconds to minutes and hours
+merge_request:
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index a15e365cc2f..956afe4faa3 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -234,6 +234,7 @@ constraints(ProjectUrlConstrainer.new) do
get :related_branches
get :can_create_branch
get :rendered_title
+ post :create_merge_request
end
collection do
post :bulk_update
diff --git a/db/migrate/20170502091007_markdown_cache_limits_to_mysql.rb b/db/migrate/20170502091007_markdown_cache_limits_to_mysql.rb
new file mode 100644
index 00000000000..008a94d8334
--- /dev/null
+++ b/db/migrate/20170502091007_markdown_cache_limits_to_mysql.rb
@@ -0,0 +1,2 @@
+# rubocop:disable all
+require_relative 'markdown_cache_limits_to_mysql'
diff --git a/db/migrate/markdown_cache_limits_to_mysql.rb b/db/migrate/markdown_cache_limits_to_mysql.rb
new file mode 100644
index 00000000000..f6686db3dc0
--- /dev/null
+++ b/db/migrate/markdown_cache_limits_to_mysql.rb
@@ -0,0 +1,13 @@
+class MarkdownCacheLimitsToMysql < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ return unless Gitlab::Database.mysql?
+
+ change_column :snippets, :content_html, :text, limit: 2147483647
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index be6684f3a6b..01c0f00c924 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170426181740) do
+ActiveRecord::Schema.define(version: 20170502091007) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 51de4fef7ff..188fbe7447d 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -40,6 +40,7 @@ Parameters:
| `owned` | boolean | no | Limit by projects owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
+| `statistics` | boolean | no | Include project statistics |
```json
[
@@ -91,7 +92,14 @@ Parameters:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "statistics": {
+ "commit_count": 37,
+ "storage_size": 1038090,
+ "repository_size": 1038090,
+ "lfs_objects_size": 0,
+ "job_artifacts_size": 0
+ }
},
{
"id": 6,
@@ -151,7 +159,14 @@ Parameters:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "statistics": {
+ "commit_count": 12,
+ "storage_size": 2066080,
+ "repository_size": 2066080,
+ "lfs_objects_size": 0,
+ "job_artifacts_size": 0
+ }
}
]
```
@@ -170,6 +185,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `statistics` | boolean | no | Include project statistics |
```json
{
@@ -241,7 +257,14 @@ Parameters:
],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "statistics": {
+ "commit_count": 37,
+ "storage_size": 1038090,
+ "repository_size": 1038090,
+ "lfs_objects_size": 0,
+ "job_artifacts_size": 0
+ }
}
```
diff --git a/doc/workflow/img/todos_icon.png b/doc/workflow/img/todos_icon.png
index 1ed16b09669..9fee4337a75 100644
--- a/doc/workflow/img/todos_icon.png
+++ b/doc/workflow/img/todos_icon.png
Binary files differ
diff --git a/doc/workflow/img/todos_index.png b/doc/workflow/img/todos_index.png
index 902a5aa6bd3..99c1575d157 100644
--- a/doc/workflow/img/todos_index.png
+++ b/doc/workflow/img/todos_index.png
Binary files differ
diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature
index 52dc15f2eb6..09094d638c9 100644
--- a/features/project/builds/artifacts.feature
+++ b/features/project/builds/artifacts.feature
@@ -17,6 +17,7 @@ Feature: Project Builds Artifacts
When I visit recent build details page
And I click artifacts browse button
Then I should see content of artifacts archive
+ And I should see the build header
Scenario: I browse subdirectory of build artifacts
Given recent build has artifacts available
@@ -25,6 +26,7 @@ Feature: Project Builds Artifacts
And I click artifacts browse button
And I click link to subdirectory within build artifacts
Then I should see content of subdirectory within artifacts archive
+ And I should see the directory name in the breadcrumb
Scenario: I browse directory with UTF-8 characters in name
Given recent build has artifacts available
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index be0f6eee55a..3ec5c8e2f4f 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -22,6 +22,12 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
end
end
+ step 'I should see the build header' do
+ page.within('.build-header') do
+ expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for commit #{@pipeline.short_sha}"
+ end
+ end
+
step 'I click link to subdirectory within build artifacts' do
page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
end
@@ -34,6 +40,12 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
end
end
+ step 'I should see the directory name in the breadcrumb' do
+ page.within('.repo-breadcrumb') do
+ expect(page).to have_content 'other_artifacts_0.1.2'
+ end
+ end
+
step 'recent build artifacts contain directory with UTF-8 characters' do
# metadata fixture contains relevant directory
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index db4b31b55bc..1ba691cbea1 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -26,6 +26,10 @@ module API
params :optional_params do
use :optional_params_ce
end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
end
resource :projects do
@@ -56,10 +60,6 @@ module API
optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
end
- params :statistics_params do
- optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
- end
-
params :create_params do
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported'
@@ -85,6 +85,7 @@ module API
end
params do
use :collection_params
+ use :statistics_params
end
get do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
@@ -151,10 +152,13 @@ module API
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
+ params do
+ use :statistics_params
+ end
get ":id" do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present user_project, with: entity, current_user: current_user,
- user_can_admin_project: can?(current_user, :admin_project, user_project)
+ user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
end
desc 'Get events for a single project' do
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index e8bb9e1f805..12458f9f410 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -128,6 +128,10 @@ module Gitlab
encode! @name
end
+ def truncated?
+ size && (size > loaded_size)
+ end
+
# Valid LFS object pointer is a text file consisting of
# version
# oid
@@ -155,10 +159,14 @@ module Gitlab
nil
end
- def truncated?
- size && (size > loaded_size)
+ def external_storage
+ return unless lfs_pointer?
+
+ :lfs
end
+ alias_method :external_size, :lfs_size
+
private
def has_lfs_version_key?
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index acd0037ee4f..e7485c22039 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -876,27 +876,6 @@ module Gitlab
rugged.remotes[remote_name].push(refspecs)
end
- # Merge the +source_name+ branch into the +target_name+ branch. This is
- # equivalent to `git merge --no_ff +source_name+`, since a merge commit
- # is always created.
- def merge(source_name, target_name, options = {})
- our_commit = rugged.branches[target_name].target
- their_commit = rugged.branches[source_name].target
-
- raise "Invalid merge target" if our_commit.nil?
- raise "Invalid merge source" if their_commit.nil?
-
- merge_index = rugged.merge_commits(our_commit, their_commit)
- return false if merge_index.conflicts?
-
- actual_options = options.merge(
- parents: [our_commit, their_commit],
- tree: merge_index.write_tree(rugged),
- update_ref: "refs/heads/#{target_name}"
- )
- Rugged::Commit.create(rugged, actual_options)
- end
-
AUTOCRLF_VALUES = {
"true" => true,
"false" => false,
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index b95cea371b9..3aac731e844 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -84,6 +84,7 @@ excluded_attributes:
- :import_jid
- :id
- :star_count
+ - :last_activity_at
snippets:
- :expired_at
merge_request_diff:
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 5476438b8fa..139ab70e125 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -65,6 +65,7 @@ namespace :gitlab do
migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines
.map { |file| Rails.root.join(file.strip).to_s }
.select { |file| File.file?(file) }
+ .select { |file| /\A[0-9]+.*\.rb\z/ =~ File.basename(file) }
Gitlab::DowntimeCheck.new.check_and_print(migrations)
end
diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake
index 6ded519aff2..761f275d42a 100644
--- a/lib/tasks/migrate/add_limits_mysql.rake
+++ b/lib/tasks/migrate/add_limits_mysql.rake
@@ -1,7 +1,9 @@
require Rails.root.join('db/migrate/limits_to_mysql')
+require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql')
desc "GitLab | Add limits to strings in mysql database"
task add_limits_mysql: :environment do
puts "Adding limits to schema.rb for mysql"
LimitsToMysql.new.up
+ MarkdownCacheLimitsToMysql.new.up
end
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 192d9d4c3ba..1bd6b339830 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -9,7 +9,6 @@ tasks = [
%w[bundle exec rake scss_lint],
%w[bundle exec rake brakeman],
%w[bundle exec license_finder],
- %w[scripts/lint-doc.sh],
%w[yarn run eslint],
%w[bundle exec rubocop --require rubocop-rspec]
]
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index d20e7368086..8f915d9d210 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::BranchesController do
controller.instance_variable_set(:@project, project)
end
- describe "POST create" do
+ describe "POST create with HTML format" do
render_views
context "on creation of a new branch" do
@@ -152,6 +152,42 @@ describe Projects::BranchesController do
end
end
+ describe 'POST create with JSON format' do
+ before do
+ sign_in(user)
+ end
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_branch name: 'my-branch', ref: 'master'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created branch' do
+ create_branch name: 'my-branch', ref: 'master'
+
+ expect(response).to match_response_schema('branch')
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns an unprocessable entity 422 response' do
+ create_branch name: "<script>alert('merge');</script>", ref: "<script>alert('ref');</script>"
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ def create_branch(name:, ref:)
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: name,
+ ref: ref,
+ format: :json
+ end
+ end
+
describe "POST destroy with HTML format" do
render_views
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 79034b8d24d..5f1f892821a 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -756,4 +756,28 @@ describe Projects::IssuesController do
expect(response).to have_http_status(200)
end
end
+
+ describe 'POST create_merge_request' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'creates a new merge request' do
+ expect { create_merge_request }.to change(project.merge_requests, :count).by(1)
+ end
+
+ it 'render merge request as json' do
+ create_merge_request
+
+ expect(response).to match_response_schema('merge_request')
+ end
+
+ def create_merge_request
+ post :create_merge_request, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: issue.to_param,
+ format: :json
+ end
+ end
end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 01b1aee4fd3..f5b54463df8 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -62,6 +62,8 @@ describe "GitLab Flavored Markdown", feature: true do
project: project,
title: "fix #{@other_issue.to_reference}",
description: "ask #{fred.to_reference} for details")
+
+ @note = create(:note_on_issue, noteable: @issue, project: @issue.project, note: "Hello world")
end
it "renders subject in issues#index" do
@@ -81,14 +83,6 @@ describe "GitLab Flavored Markdown", feature: true do
expect(page).to have_link(fred.to_reference)
end
-
- it "renders updated subject once edited somewhere else in issues#show" do
- visit namespace_project_issue_path(project.namespace, project, @issue)
- @issue.update(title: "fix #{@other_issue.to_reference} and update")
-
- wait_for_vue_resource
- expect(page).to have_text("fix #{@other_issue.to_reference} and update")
- end
end
describe "for merge requests" do
diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb
new file mode 100644
index 00000000000..44c19275ae5
--- /dev/null
+++ b/spec/features/issues/create_branch_merge_request_spec.rb
@@ -0,0 +1,91 @@
+require 'rails_helper'
+
+feature 'Create Branch/Merge Request Dropdown on issue page', feature: true, js: true do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
+
+ context 'for team members' do
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+ end
+
+ it 'allows creating a merge request from the issue page' do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ select_dropdown_option('create-mr')
+
+ wait_for_ajax
+
+ expect(page).to have_content("created branch 1-cherry-coloured-funk")
+ expect(page).to have_content("mentioned in merge request !1")
+
+ visit namespace_project_merge_request_path(project.namespace, project, MergeRequest.first)
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(current_path).to eq(namespace_project_merge_request_path(project.namespace, project, MergeRequest.first))
+ end
+
+ it 'allows creating a branch from the issue page' do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ select_dropdown_option('create-branch')
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
+ expect(current_path).to eq namespace_project_tree_path(project.namespace, project, '1-cherry-coloured-funk')
+ end
+
+ context "when there is a referenced merge request" do
+ let!(:note) do
+ create(:note, :on_issue, :system, project: project, noteable: issue,
+ note: "mentioned in #{referenced_mr.to_reference}")
+ end
+
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes #{issue.to_reference}", author: user)
+ end
+
+ before do
+ referenced_mr.cache_merge_request_closes_issues!(user)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'disables the create branch button' do
+ expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
+ expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+
+ context 'when issue is confidential' do
+ it 'disables the create branch button' do
+ issue = create(:issue, :confidential, project: project)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).not_to have_css('.create-mr-dropdown-wrap')
+ end
+ end
+ end
+
+ context 'for visitors' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'shows no buttons' do
+ expect(page).not_to have_selector('.create-mr-dropdown-wrap')
+ end
+ end
+
+ def select_dropdown_option(option)
+ find('.create-mr-dropdown-wrap .dropdown-toggle').click
+ find("li[data-value='#{option}']").click
+ find('.js-create-merge-request').click
+ end
+end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
deleted file mode 100644
index c0ab42c6822..00000000000
--- a/spec/features/issues/new_branch_button_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'rails_helper'
-
-feature 'Start new branch from an issue', feature: true, js: true do
- let!(:project) { create(:project) }
- let!(:issue) { create(:issue, project: project) }
- let!(:user) { create(:user)}
-
- context "for team members" do
- before do
- project.team << [user, :master]
- login_as(user)
- end
-
- it 'shows the new branch button' do
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- expect(page).to have_css('#new-branch .available')
- end
-
- context "when there is a referenced merge request" do
- let!(:note) do
- create(:note, :on_issue, :system, project: project, noteable: issue,
- note: "mentioned in #{referenced_mr.to_reference}")
- end
-
- let(:referenced_mr) do
- create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes #{issue.to_reference}", author: user)
- end
-
- before do
- referenced_mr.cache_merge_request_closes_issues!(user)
-
- visit namespace_project_issue_path(project.namespace, project, issue)
- end
-
- it "hides the new branch button" do
- expect(page).to have_css('#new-branch .unavailable')
- expect(page).not_to have_css('#new-branch .available')
- expect(page).to have_content /1 Related Merge Request/
- end
- end
-
- context 'when issue is confidential' do
- it 'hides the new branch button' do
- issue = create(:issue, :confidential, project: project)
-
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- expect(page).not_to have_css('#new-branch')
- end
- end
- end
-
- context 'for visitors' do
- it 'shows no buttons' do
- visit namespace_project_issue_path(project.namespace, project, issue)
-
- expect(page).not_to have_css('#new-branch')
- end
- end
-end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 378f6de1a78..58b3215f14c 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -4,14 +4,77 @@ feature 'Issue notes polling', :feature, :js do
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
- before do
- visit namespace_project_issue_path(project.namespace, project, issue)
+ describe 'creates' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'displays the new comment' do
+ note = create(:note, noteable: issue, project: project, note: 'Looks good!')
+ page.execute_script('notes.refresh();')
+
+ expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
+ end
end
- it 'should display the new comment' do
- note = create(:note, noteable: issue, project: project, note: 'Looks good!')
- page.execute_script('notes.refresh();')
+ describe 'updates' do
+ let(:user) { create(:user) }
+ let(:note_text) { "Hello World" }
+ let(:updated_text) { "Bye World" }
+ let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
+
+ before do
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'displays the updated content' do
+ expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ end
+
+ it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
+ find("#note_#{existing_note.id} .js-note-edit").click
+
+ expect(page).to have_field("note[note]", with: note_text)
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_field("note[note]", with: updated_text)
+ end
+
+ it 'when editing but you changed some things, and an update comes in, show a warning' do
+ find("#note_#{existing_note.id} .js-note-edit").click
- expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
+ expect(page).to have_field("note[note]", with: note_text)
+
+ find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_selector(".alert")
+ end
+
+ it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
+ find("#note_#{existing_note.id} .js-note-edit").click
+
+ expect(page).to have_field("note[note]", with: note_text)
+
+ find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+ update_note(existing_note, updated_text)
+
+ find("#note_#{existing_note.id} .note-edit-cancel").click
+
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ end
+ end
+
+ def update_note(note, new_text)
+ note.update(note: new_text)
+ page.execute_script('notes.refresh();')
end
end
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 7a2da623c58..2b5b803946c 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -24,7 +24,12 @@ feature 'Merge Request versions', js: true, feature: true do
before do
page.within '.mr-version-dropdown' do
find('.btn-default').click
- find(:link, 'version 1').trigger('click')
+ click_link 'version 1'
+ end
+
+ # Wait for the page to load
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'version 1'
end
end
@@ -36,8 +41,8 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '5 changed files'
end
- it 'show the message about disabled comment creation' do
- expect(page).to have_content 'comment creation is disabled'
+ it 'show the message about comments' do
+ expect(page).to have_content 'Not all comments are displayed'
end
it 'shows comments that were last relevant at that version' do
@@ -52,15 +57,41 @@ feature 'Merge Request versions', js: true, feature: true do
outdated_diff_note.position = outdated_diff_note.original_position
outdated_diff_note.save!
+ visit current_url
+
expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
+
+ it 'allows commenting' do
+ diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
+ line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
+
+ page.within(diff_file_selector) do
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
+ find(".line_holder[id='#{line_code}'] button").trigger 'click'
+
+ page.within("form[data-line-code='#{line_code}']") do
+ fill_in "note[note]", with: "Typo, please fix"
+ find(".js-comment-button").click
+ end
+
+ wait_for_ajax
+
+ expect(page).to have_content("Typo, please fix")
+ end
+ end
end
describe 'compare with older version' do
before do
page.within '.mr-version-compare-dropdown' do
find('.btn-default').click
- find(:link, 'version 1').trigger('click')
+ click_link 'version 1'
+ end
+
+ # Wait for the page to load
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 1'
end
end
@@ -80,8 +111,43 @@ feature 'Merge Request versions', js: true, feature: true do
end
end
- it 'show the message about disabled comments' do
- expect(page).to have_content 'Comments are disabled'
+ it 'show the message about comments' do
+ expect(page).to have_content 'Not all comments are displayed'
+ end
+
+ it 'shows comments that were last relevant at that version' do
+ position = Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ outdated_diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+ visit current_url
+ wait_for_ajax
+
+ expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
+ end
+
+ it 'allows commenting' do
+ diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
+ line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
+
+ page.within(diff_file_selector) do
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
+ find(".line_holder[id='#{line_code}'] button").trigger 'click'
+
+ page.within("form[data-line-code='#{line_code}']") do
+ fill_in "note[note]", with: "Typo, please fix"
+ find(".js-comment-button").click
+ end
+
+ wait_for_ajax
+
+ expect(page).to have_content("Typo, please fix")
+ end
end
it 'show diff between new and old version' do
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 8dba2ccbafa..5955623f565 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -159,7 +159,7 @@ feature 'File blob', :js, feature: true do
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows an error message
- expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can view the source or download it instead.')
+ expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can download it instead.')
# shows a viewer switcher
expect(page).to have_selector('.js-blob-viewer-switcher')
@@ -167,8 +167,8 @@ feature 'File blob', :js, feature: true do
# does not show a copy button
expect(page).not_to have_selector('.js-copy-blob-source-btn')
- # shows a raw button
- expect(page).to have_link('Open raw')
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
@@ -332,4 +332,41 @@ feature 'File blob', :js, feature: true do
end
end
end
+
+ context 'empty file' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add empty file",
+ file_path: 'files/empty.md',
+ file_content: ''
+ ).execute
+
+ visit_blob('files/empty.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # shows an error message
+ expect(page).to have_content('Empty file')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # does not show a download or raw button
+ expect(page).not_to have_link('Download')
+ expect(page).not_to have_link('Open raw')
+ end
+ end
+ end
end
diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb
index 5b24ac0292b..a04fbcdd15f 100644
--- a/spec/features/protected_tags/access_control_ce_spec.rb
+++ b/spec/features/protected_tags/access_control_ce_spec.rb
@@ -10,8 +10,8 @@ RSpec.shared_examples "protected tags > access control > CE" do
unless allowed_to_create_button.text == access_type_name
allowed_to_create_button.click
- find('.dropdown.open .dropdown-menu li', match: :first)
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ find('.create_access_levels-container .dropdown-menu li', match: :first)
+ within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index e3aa87ded28..e68448467b0 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -11,6 +11,7 @@ feature 'Projected Tags', feature: true, js: true do
find(".js-protected-tag-select").click
find(".dropdown-input-field").set(tag_name)
click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
end
describe "explicit protected tags" do
diff --git a/spec/fixtures/api/schemas/branch.json b/spec/fixtures/api/schemas/branch.json
new file mode 100644
index 00000000000..0bb74577010
--- /dev/null
+++ b/spec/fixtures/api/schemas/branch.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "url"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "url": { "type": "uri" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/merge_request.json b/spec/fixtures/api/schemas/merge_request.json
new file mode 100644
index 00000000000..36962660cd9
--- /dev/null
+++ b/spec/fixtures/api/schemas/merge_request.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "iid",
+ "url"
+ ],
+ "properties" : {
+ "iid": { "type": "integer" },
+ "url": { "type": "uri" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 075f1887d91..1b4393e6167 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -145,7 +145,7 @@ describe BlobHelper do
end
end
- context 'for error :server_side_but_stored_in_lfs' do
+ context 'for error :server_side_but_stored_externally' do
let(:blob) { fake_blob(lfs: true) }
it 'returns an error message' do
@@ -183,40 +183,56 @@ describe BlobHelper do
expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
end
end
- end
- context 'when the viewer is rich' do
- context 'the blob is rendered as text' do
- let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+ context 'when the viewer is rich' do
+ context 'the blob is rendered as text' do
+ let(:blob) { fake_blob(path: 'file.md', size: 2.megabytes) }
+
+ it 'includes a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/view the source/)
+ end
+ end
+
+ context 'the blob is not rendered as text' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 2.megabytes) }
- it 'includes a "view the source" link' do
- expect(helper.blob_render_error_options(viewer)).to include(/view the source/)
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
end
end
- context 'the blob is not rendered as text' do
- let(:blob) { fake_blob(path: 'file.pdf', binary: true, lfs: true) }
+ context 'when the viewer is not rich' do
+ before do
+ viewer_class.type = :simple
+ end
+
+ let(:blob) { fake_blob(path: 'file.md', size: 2.megabytes) }
it 'does not include a "view the source" link' do
expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
end
end
- end
- context 'when the viewer is not rich' do
- before do
- viewer_class.type = :simple
+ it 'includes a "download it" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/download it/)
end
+ end
+ context 'for error :server_side_but_stored_externally' do
let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+ it 'does not include a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
+ end
+
it 'does not include a "view the source" link' do
expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
end
- end
- it 'includes a "download it" link' do
- expect(helper.blob_render_error_options(viewer)).to include(/download it/)
+ it 'includes a "download it" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/download it/)
+ end
end
end
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index a427de32c4c..6c990f94175 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe NotesHelper do
+ include RepoHelpers
+
let(:owner) { create(:owner) }
let(:group) { create(:group) }
let(:project) { create(:empty_project, namespace: group) }
@@ -36,4 +38,141 @@ describe NotesHelper do
expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
end
end
+
+ describe '#discussion_path' do
+ let(:project) { create(:project) }
+
+ context 'for a merge request discusion' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'for a diff discussion' do
+ context 'when the discussion is active' do
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ it 'returns the diff path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion is on an older merge request version' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+ let(:discussion) { diff_note.to_discussion }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns the diff version path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: merge_request_diff1, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion is on a comparison between merge request versions' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ end
+
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position).to_discussion }
+
+ it 'returns the diff version comparison path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: merge_request_diff3, start_sha: merge_request_diff1.head_commit_sha, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion does not have a merge request version' do
+ let(:outdated_diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, diff_refs: project.commit(sample_commit.id).diff_refs) }
+ let(:discussion) { outdated_diff_note.to_discussion }
+
+ before do
+ outdated_diff_note.position = outdated_diff_note.original_position
+ outdated_diff_note.save!
+ end
+
+ it 'returns nil' do
+ expect(helper.discussion_path(discussion)).to be_nil
+ end
+ end
+ end
+
+ context 'for a legacy diff discussion' do
+ let(:discussion) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ context 'when the discussion is active' do
+ before do
+ allow(discussion).to receive(:active?).and_return(true)
+ end
+
+ it 'returns the diff path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: discussion.line_code))
+ end
+ end
+
+ context 'when the discussion is outdated' do
+ before do
+ allow(discussion).to receive(:active?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(helper.discussion_path(discussion)).to be_nil
+ end
+ end
+ end
+
+ context 'for a non-diff discussion' do
+ let(:discussion) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ it 'returns nil' do
+ expect(helper.discussion_path(discussion)).to be_nil
+ end
+ end
+ end
+
+ context 'for a commit discussion' do
+ let(:commit) { discussion.noteable }
+
+ context 'for a diff discussion' do
+ let(:discussion) { create(:diff_note_on_commit, project: project).to_discussion }
+
+ it 'returns the commit path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(namespace_project_commit_path(project.namespace, project, commit, anchor: discussion.line_code))
+ end
+ end
+
+ context 'for a legacy diff discussion' do
+ let(:discussion) { create(:legacy_diff_note_on_commit, project: project).to_discussion }
+
+ it 'returns the commit path with the line code' do
+ expect(helper.discussion_path(discussion)).to eq(namespace_project_commit_path(project.namespace, project, commit, anchor: discussion.line_code))
+ end
+ end
+
+ context 'for a non-diff discussion' do
+ let(:discussion) { create(:discussion_note_on_commit, project: project).to_discussion }
+
+ it 'returns the commit path' do
+ expect(helper.discussion_path(discussion)).to eq(namespace_project_commit_path(project.namespace, project, commit))
+ end
+ end
+ end
+ end
end
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 9a2570ef7e9..0fd573eae3f 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -108,8 +108,8 @@ describe('Issue', function() {
expect(this.$triggeredButton).toHaveProp('disabled', true);
expectNewBranchButtonState(true, false);
return this.issueStateDeferred;
- } else if (req.url === Issue.$btnNewBranch.data('path')) {
- expect(req.type).toBe('get');
+ } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
+ expect(req.type).toBe('GET');
expectNewBranchButtonState(true, false);
return this.canCreateBranchDeferred;
}
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ca8ee04d955..cdc5c4510ff 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,10 +1,12 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-require('~/notes');
-require('vendor/autosize');
-require('~/gl_form');
-require('~/lib/utils/text_utility');
+import 'vendor/autosize';
+import '~/gl_form';
+import '~/lib/utils/text_utility';
+import '~/render_gfm';
+import '~/render_math';
+import '~/notes';
(function() {
window.gon || (window.gon = {});
@@ -80,35 +82,78 @@ require('~/lib/utils/text_utility');
beforeEach(() => {
note = {
+ id: 1,
discussion_html: null,
valid: true,
- html: '<div></div>',
+ note: 'heya',
+ html: '<div>heya</div>',
};
- $notesList = jasmine.createSpyObj('$notesList', ['find']);
+ $notesList = jasmine.createSpyObj('$notesList', [
+ 'find',
+ 'append',
+ ]);
notes = jasmine.createSpyObj('notes', [
'refresh',
'isNewNote',
+ 'isUpdatedNote',
'collapseLongCommitList',
'updateNotesCount',
+ 'putConflictEditWarningInPlace'
]);
notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
notes.note_ids = [];
+ notes.updatedNotesTrackingMap = {};
- spyOn(window, '$').and.returnValue($notesList);
spyOn(gl.utils, 'localTimeAgo');
- spyOn(Notes, 'animateAppendNote');
- notes.isNewNote.and.returnValue(true);
-
- Notes.prototype.renderNote.call(notes, note);
+ spyOn(Notes, 'animateAppendNote').and.callThrough();
+ spyOn(Notes, 'animateUpdateNote').and.callThrough();
});
- it('should query for the notes list', () => {
- expect(window.$).toHaveBeenCalledWith('ul.main-notes-list');
+ describe('when adding note', () => {
+ it('should call .animateAppendNote', () => {
+ notes.isNewNote.and.returnValue(true);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ });
});
- it('should call .animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ describe('when note was edited', () => {
+ it('should call .animateUpdateNote', () => {
+ notes.isUpdatedNote.and.returnValue(true);
+ const $note = $('<div>');
+ $notesList.find.and.returnValue($note);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
+ });
+
+ describe('while editing', () => {
+ it('should update textarea if nothing has been touched', () => {
+ notes.isUpdatedNote.and.returnValue(true);
+ const $note = $(`<div class="is-editing">
+ <div class="original-note-content">initial</div>
+ <textarea class="js-note-text">initial</textarea>
+ </div>`);
+ $notesList.find.and.returnValue($note);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect($note.find('.js-note-text').val()).toEqual(note.note);
+ });
+
+ it('should call .putConflictEditWarningInPlace', () => {
+ notes.isUpdatedNote.and.returnValue(true);
+ const $note = $(`<div class="is-editing">
+ <div class="original-note-content">initial</div>
+ <textarea class="js-note-text">different</textarea>
+ </div>`);
+ $notesList.find.and.returnValue($note);
+ Notes.prototype.renderNote.call(notes, note, null, $notesList);
+
+ expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
+ });
+ });
});
});
@@ -147,14 +192,12 @@ require('~/lib/utils/text_utility');
});
describe('Discussion root note', () => {
- let $notesList;
let body;
beforeEach(() => {
body = jasmine.createSpyObj('body', ['attr']);
discussionContainer = { length: 0 };
- spyOn(window, '$').and.returnValues(discussionContainer, body, $notesList);
$form.closest.and.returnValues(row, $form);
$form.find.and.returnValues(discussionContainer);
body.attr.and.returnValue('');
@@ -162,12 +205,8 @@ require('~/lib/utils/text_utility');
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
- it('should query for the notes list', () => {
- expect(window.$.calls.argsFor(2)).toEqual(['ul.main-notes-list']);
- });
-
it('should call Notes.animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $notesList);
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
});
});
@@ -175,16 +214,12 @@ require('~/lib/utils/text_utility');
beforeEach(() => {
discussionContainer = { length: 1 };
- spyOn(window, '$').and.returnValues(discussionContainer);
- $form.closest.and.returnValues(row);
+ $form.closest.and.returnValues(row, $form);
+ $form.find.and.returnValues(discussionContainer);
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
- it('should query foor the discussion container', () => {
- expect(window.$).toHaveBeenCalledWith(`.notes[data-discussion-id="${note.discussion_id}"]`);
- });
-
it('should call Notes.animateAppendNote', () => {
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
});
@@ -193,35 +228,45 @@ require('~/lib/utils/text_utility');
describe('animateAppendNote', () => {
let noteHTML;
- let $note;
let $notesList;
+ let $resultantNote;
beforeEach(() => {
noteHTML = '<div></div>';
- $note = jasmine.createSpyObj('$note', ['addClass', 'renderGFM', 'removeClass']);
$notesList = jasmine.createSpyObj('$notesList', ['append']);
- spyOn(window, '$').and.returnValue($note);
- spyOn(window, 'setTimeout').and.callThrough();
- $note.addClass.and.returnValue($note);
- $note.renderGFM.and.returnValue($note);
+ $resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
+ });
- Notes.animateAppendNote(noteHTML, $notesList);
+ it('should have `fade-in` class', () => {
+ expect($resultantNote.hasClass('fade-in')).toEqual(true);
});
- it('should init the note jquery object', () => {
- expect(window.$).toHaveBeenCalledWith(noteHTML);
+ it('should append note to the notes list', () => {
+ expect($notesList.append).toHaveBeenCalledWith($resultantNote);
});
+ });
+
+ describe('animateUpdateNote', () => {
+ let noteHTML;
+ let $note;
+ let $updatedNote;
- it('should call addClass', () => {
- expect($note.addClass).toHaveBeenCalledWith('fade-in');
+ beforeEach(() => {
+ noteHTML = '<div></div>';
+ $note = jasmine.createSpyObj('$note', [
+ 'replaceWith'
+ ]);
+
+ $updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
- it('should call renderGFM', () => {
- expect($note.renderGFM).toHaveBeenCalledWith();
+
+ it('should have `fade-in` class', () => {
+ expect($updatedNote.hasClass('fade-in')).toEqual(true);
});
- it('should append note to the notes list', () => {
- expect($notesList.append).toHaveBeenCalledWith($note);
+ it('should call replaceWith on $note', () => {
+ expect($note.replaceWith).toHaveBeenCalledWith($updatedNote);
});
});
});
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 6e145947104..1035428b2e7 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
- let(:project) { setup_project }
+ let!(:project) { setup_project }
before do
project.team << [user, :master]
@@ -219,7 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
releases: [release],
group: group
)
- project.update(description_html: 'description')
+ project.update_column(:description_html, 'description')
project_label = create(:label, project: project)
group_label = create(:group_label, group: group)
create(:label_link, label: project_label, target: issue)
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 7e8a1c8add7..f84c6b48173 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -35,8 +35,68 @@ describe Blob do
end
end
+ describe '#external_storage_error?' do
+ context 'if the blob is stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ context 'when the project has LFS enabled' do
+ it 'returns false' do
+ expect(blob.external_storage_error?).to be_falsey
+ end
+ end
+
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
+
+ it 'returns true' do
+ expect(blob.external_storage_error?).to be_truthy
+ end
+ end
+ end
+
+ context 'if the blob is not stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.md') }
+
+ it 'returns false' do
+ expect(blob.external_storage_error?).to be_falsey
+ end
+ end
+ end
+
+ describe '#stored_externally?' do
+ context 'if the blob is stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ context 'when the project has LFS enabled' do
+ it 'returns true' do
+ expect(blob.stored_externally?).to be_truthy
+ end
+ end
+
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
+
+ it 'returns false' do
+ expect(blob.stored_externally?).to be_falsey
+ end
+ end
+ end
+
+ context 'if the blob is not stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.md') }
+
+ it 'returns false' do
+ expect(blob.stored_externally?).to be_falsey
+ end
+ end
+ end
+
describe '#raw_binary?' do
- context 'if the blob is a valid LFS pointer' do
+ context 'if the blob is stored externally' do
context 'if the extension has a rich viewer' do
context 'if the viewer is binary' do
it 'returns true' do
@@ -56,15 +116,63 @@ describe Blob do
end
context "if the extension doesn't have a rich viewer" do
- it 'returns true' do
- blob = fake_blob(path: 'file.exe', lfs: true)
+ context 'if the extension has a text mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.txt', lfs: true)
- expect(blob.raw_binary?).to be_truthy
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.ics', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
+
+ context 'if the extension has a binary mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.rb', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.exe', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+ end
+
+ context 'if the extension has an unknown mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.ini', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.wtf', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
end
end
end
- context 'if the blob is not an LFS pointer' do
+ context 'if the blob is not stored externally' do
context 'if the blob is binary' do
it 'returns true' do
blob = fake_blob(path: 'file.pdf', binary: true)
@@ -94,7 +202,7 @@ describe Blob do
describe '#simple_viewer' do
context 'when the blob is empty' do
it 'returns an empty viewer' do
- blob = fake_blob(data: '')
+ blob = fake_blob(data: '', size: 0)
expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
end
@@ -118,7 +226,7 @@ describe Blob do
end
describe '#rich_viewer' do
- context 'when the blob is an invalid LFS pointer' do
+ context 'when the blob has an external storage error' do
before do
project.lfs_enabled = false
end
@@ -138,7 +246,7 @@ describe Blob do
end
end
- context 'when the blob is a valid LFS pointer' do
+ context 'when the blob is stored externally' do
it 'returns a matching viewer' do
blob = fake_blob(path: 'file.pdf', lfs: true)
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index a3e598de56d..740ad9d275e 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -139,7 +139,7 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the viewer is server side but the blob is stored in LFS' do
+ context 'when the viewer is server side but the blob is stored externally' do
let(:project) { build(:empty_project, lfs_enabled: true) }
let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
@@ -148,8 +148,8 @@ describe BlobViewer::Base, model: true do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
- it 'return :server_side_but_stored_in_lfs' do
- expect(viewer.render_error).to eq(:server_side_but_stored_in_lfs)
+ it 'return :server_side_but_stored_externally' do
+ expect(viewer.render_error).to eq(:server_side_but_stored_externally)
end
end
end
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
index 48e7c0a822c..81f338745b1 100644
--- a/spec/models/diff_discussion_spec.rb
+++ b/spec/models/diff_discussion_spec.rb
@@ -1,19 +1,86 @@
require 'spec_helper'
describe DiffDiscussion, model: true do
- subject { described_class.new([first_note, second_note, third_note]) }
+ include RepoHelpers
- let(:first_note) { create(:diff_note_on_merge_request) }
- let(:merge_request) { first_note.noteable }
- let(:project) { first_note.project }
- let(:second_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
- let(:third_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
+ subject { described_class.new([diff_note]) }
+
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
describe '#reply_attributes' do
it 'includes position and original_position' do
attributes = subject.reply_attributes
- expect(attributes[:position]).to eq(first_note.position.to_json)
- expect(attributes[:original_position]).to eq(first_note.original_position.to_json)
+ expect(attributes[:position]).to eq(diff_note.position.to_json)
+ expect(attributes[:original_position]).to eq(diff_note.original_position.to_json)
+ end
+ end
+
+ describe '#merge_request_version_params' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'when the discussion is active' do
+ it 'returns an empty hash, which will end up showing the latest version' do
+ expect(subject.merge_request_version_params).to eq({})
+ end
+ end
+
+ context 'when the discussion is on an older merge request version' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns the diff ID for the version to show' do
+ expect(diff_id: merge_request_diff1.id)
+ end
+ end
+
+ context 'when the discussion is on a comparison between merge request versions' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+
+ it 'returns the diff ID and start sha of the versions to compare' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
+ end
+ end
+
+ context 'when the discussion does not have a merge request version' do
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, diff_refs: project.commit(sample_commit.id).diff_refs) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns nil' do
+ expect(subject.merge_request_version_params).to be_nil
+ end
end
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index f32b6b99b3d..ab4c51a87b0 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -155,23 +155,6 @@ describe DiffNote, models: true do
end
end
- describe '#latest_merge_request_diff' do
- context 'when active' do
- it 'returns the current merge request diff' do
- expect(subject.latest_merge_request_diff).to eq(merge_request.merge_request_diff)
- end
- end
-
- context 'when outdated' do
- let!(:old_merge_request_diff) { merge_request.merge_request_diff }
- let!(:new_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: commit.diff_refs) }
-
- it 'returns the latest merge request diff that this diff note applied to' do
- expect(subject.latest_merge_request_diff).to eq(old_merge_request_diff)
- end
- end
- end
-
describe "creation" do
describe "updating of position" do
context "when noteable is a commit" do
@@ -256,4 +239,39 @@ describe DiffNote, models: true do
end
end
end
+
+ describe '#created_at_diff?' do
+ let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: diff_refs
+ )
+ end
+
+ context "when noteable is a commit" do
+ subject { build(:diff_note_on_commit, project: project, position: position) }
+
+ it "returns true" do
+ expect(subject.created_at_diff?(diff_refs)).to be true
+ end
+ end
+
+ context "when noteable is a merge request" do
+ context "when the diff refs match the original one of the diff note" do
+ it "returns true" do
+ expect(subject.created_at_diff?(diff_refs)).to be true
+ end
+ end
+
+ context "when the diff refs don't match the original one of the diff note" do
+ it "returns false" do
+ expect(subject.created_at_diff?(merge_request.diff_refs)).to be false
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 11befd4edfe..8748b98a4e3 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -291,6 +291,27 @@ describe Issue, models: true do
end
end
+ describe '#has_related_branch?' do
+ let(:issue) { create(:issue, title: "Blue Bell Knoll") }
+ subject { issue.has_related_branch? }
+
+ context 'branch found' do
+ before do
+ allow(issue.project.repository).to receive(:branch_names).and_return(["iceblink-luck", issue.to_branch_name])
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'branch not found' do
+ before do
+ allow(issue.project.repository).to receive(:branch_names).and_return(["lazy-calm"])
+ end
+
+ it { is_expected.to eq false }
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:issue, project: create(:project, :repository)) }
diff --git a/spec/models/legacy_diff_discussion_spec.rb b/spec/models/legacy_diff_discussion_spec.rb
index 153e757a0ef..6eb4a2aaf39 100644
--- a/spec/models/legacy_diff_discussion_spec.rb
+++ b/spec/models/legacy_diff_discussion_spec.rb
@@ -8,4 +8,26 @@ describe LegacyDiffDiscussion, models: true do
expect(subject.reply_attributes[:line_code]).to eq(subject.line_code)
end
end
+
+ describe '#merge_request_version_params' do
+ context 'when the discussion is active' do
+ before do
+ allow(subject).to receive(:active?).and_return(true)
+ end
+
+ it 'returns an empty hash, which will end up showing the latest version' do
+ expect(subject.merge_request_version_params).to eq({})
+ end
+ end
+
+ context 'when the discussion is outdated' do
+ before do
+ allow(subject).to receive(:active?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(subject.merge_request_version_params).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index be08b96641a..8b72125dd5d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1554,4 +1554,23 @@ describe MergeRequest, models: true do
expect(subject.has_no_commits?).to be_truthy
end
end
+
+ describe '#merge_request_diff_for' do
+ subject { create(:merge_request, importing: true) }
+ let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'with diff refs' do
+ it 'returns the diffs' do
+ expect(subject.merge_request_diff_for(merge_request_diff1.diff_refs)).to eq(merge_request_diff1)
+ end
+ end
+
+ context 'with a commit SHA' do
+ it 'returns the diffs' do
+ expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 557ea97b008..7a01cef9b4b 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -272,9 +272,9 @@ describe Note, models: true do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
- old_line: 16,
- new_line: 22,
- diff_refs: merge_request.diff_refs
+ old_line: nil,
+ new_line: 13,
+ diff_refs: project.commit(sample_commit.id).diff_refs
)
end
@@ -288,26 +288,78 @@ describe Note, models: true do
)
end
- subject { merge_request.notes.grouped_diff_discussions }
+ context 'active diff discussions' do
+ subject { merge_request.notes.grouped_diff_discussions }
- it "includes active discussions" do
- discussions = subject.values.flatten
+ it "includes active discussions" do
+ discussions = subject.values.flatten
- expect(discussions.count).to eq(2)
- expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
- expect(discussions.all?(&:active?)).to be true
+ expect(discussions.count).to eq(2)
+ expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+ expect(discussions.all?(&:active?)).to be true
- expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
- expect(discussions.last.notes).to eq([active_diff_note3])
- end
+ expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+ expect(discussions.last.notes).to eq([active_diff_note3])
+ end
- it "doesn't include outdated discussions" do
- expect(subject.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ it "doesn't include outdated discussions" do
+ expect(subject.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ end
+
+ it "groups the discussions by line code" do
+ expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
+ expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
end
- it "groups the discussions by line code" do
- expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
- expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ context 'diff discussions for older diff refs' do
+ subject { merge_request.notes.grouped_diff_discussions(diff_refs) }
+
+ context 'for diff refs a discussion was created at' do
+ let(:diff_refs) { active_position2.diff_refs }
+
+ it "includes discussions that were created then" do
+ discussions = subject.values.flatten
+
+ expect(discussions.count).to eq(1)
+
+ discussion = discussions.first
+
+ expect(discussion.id).to eq(active_diff_note3.discussion_id)
+ expect(discussion.active?).to be true
+ expect(discussion.active?(diff_refs)).to be false
+ expect(discussion.created_at_diff?(diff_refs)).to be true
+
+ expect(discussion.notes).to eq([active_diff_note3])
+ end
+
+ it "groups the discussions by original line code" do
+ expect(subject[active_diff_note3.original_line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
+ end
+
+ context 'for diff refs a discussion was last active at' do
+ let(:diff_refs) { outdated_position.diff_refs }
+
+ it "includes discussions that were last active" do
+ discussions = subject.values.flatten
+
+ expect(discussions.count).to eq(1)
+
+ discussion = discussions.first
+
+ expect(discussion.id).to eq(outdated_diff_note1.discussion_id)
+ expect(discussion.active?).to be false
+ expect(discussion.active?(diff_refs)).to be true
+ expect(discussion.created_at_diff?(diff_refs)).to be true
+
+ expect(discussion.notes).to eq([outdated_diff_note1, outdated_diff_note2])
+ end
+
+ it "groups the discussions by line code" do
+ expect(subject[outdated_diff_note1.line_code].first.id).to eq(outdated_diff_note1.discussion_id)
+ end
+ end
end
end
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index ec5c6c5e0ed..e005be42b0d 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -4,6 +4,7 @@ describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
let(:user) { { name: 'hacker' } }
+ let(:duration) { 7210 }
let(:args) do
{
object_attributes: {
@@ -26,7 +27,6 @@ describe ChatMessage::PipelineMessage do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
@@ -39,7 +39,6 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:duration) { 10 }
let(:message) { build_message }
it 'returns a message with information about failed build' do
@@ -64,7 +63,7 @@ describe ChatMessage::PipelineMessage do
"<http://example.gitlab.com|project_name>:" \
" Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
" of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ " by #{name} #{status_text} in 02:00:10"
end
end
@@ -76,7 +75,6 @@ describe ChatMessage::PipelineMessage do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:duration) { 10 }
let(:message) { build_markdown_message('passed') }
it 'returns a message with information about succeeded build' do
@@ -85,7 +83,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker passed',
subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 10 seconds',
+ text: 'in 02:00:10',
image: ''
})
end
@@ -94,7 +92,6 @@ describe ChatMessage::PipelineMessage do
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:duration) { 10 }
let(:message) { build_markdown_message }
it 'returns a message with information about failed build' do
@@ -103,7 +100,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 10 seconds',
+ text: 'in 02:00:10',
image: ''
})
end
@@ -118,7 +115,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by API failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
- text: 'in 10 seconds',
+ text: 'in 02:00:10',
image: ''
})
end
@@ -129,7 +126,7 @@ describe ChatMessage::PipelineMessage do
"[project_name](http://example.gitlab.com):" \
" Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of [develop](http://example.gitlab.com/commits/develop)" \
- " branch by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ " branch by #{name} #{status_text} in 02:00:10"
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 5216764a82d..dd6514b3b50 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1098,21 +1098,33 @@ describe Repository, models: true do
end
describe '#merge' do
- it 'merges the code and return the commit id' do
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
+
+ let(:commit_options) do
+ author = repository.user_to_committer(user)
+ { message: 'Test \r\n\r\n message', committer: author, author: author }
+ end
+
+ it 'merges the code and returns the commit id' do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
-
- merge_commit_id = repository.merge(user,
- merge_request.diff_head_sha,
- merge_request,
- commit_options)
+ merge_commit_id = merge(repository, user, merge_request, commit_options)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
+
+ it 'removes carriage returns from commit message' do
+ merge_commit_id = merge(repository, user, merge_request, commit_options)
+
+ expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r"))
+ end
+
+ def merge(repository, user, merge_request, options = {})
+ repository.merge(user, merge_request.diff_head_sha, merge_request, options)
+ end
end
describe '#revert' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index cc03d7a933b..ab70ce5cd2f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -665,6 +665,20 @@ describe API::Projects do
})
end
+ it "does not include statistics by default" do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).not_to include 'statistics'
+ end
+
+ it "includes statistics if requested" do
+ get api("/projects/#{project.id}", user), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to include 'statistics'
+ end
+
describe 'permissions' do
context 'all projects' do
before { project.team << [user, :master] }
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
new file mode 100644
index 00000000000..1588d30c394
--- /dev/null
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe MergeRequests::CreateFromIssueService, services: true do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+
+ subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ it 'returns an error with invalid issue iid' do
+ result = described_class.new(project, user, issue_iid: -1).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'Invalid issue iid'
+ end
+
+ it 'delegates issue search to IssuesFinder' do
+ expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+ described_class.new(project, user, issue_iid: -1).execute
+ end
+
+ it 'delegates the branch creation to CreateBranchService' do
+ expect_any_instance_of(CreateBranchService).to receive(:execute).once.and_call_original
+
+ service.execute
+ end
+
+ it 'creates a branch based on issue title' do
+ service.execute
+
+ expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
+ end
+
+ it 'creates a system note' do
+ expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
+
+ service.execute
+ end
+
+ it 'creates a merge request' do
+ expect { service.execute }.to change(project.merge_requests, :count).by(1)
+ end
+
+ it 'sets the merge request title to: "WIP: Resolves "$issue-title"' do
+ result = service.execute
+
+ expect(result[:merge_request].title).to eq("WIP: Resolve \"#{issue.title}\"")
+ end
+
+ it 'sets the merge request author to current user' do
+ result = service.execute
+
+ expect(result[:merge_request].author).to eq user
+ end
+
+ it 'sets the merge request source branch to the new issue branch' do
+ result = service.execute
+
+ expect(result[:merge_request].source_branch).to eq issue.to_branch_name
+ end
+
+ it 'sets the merge request target branch to the project default branch' do
+ result = service.execute
+
+ expect(result[:merge_request].target_branch).to eq project.default_branch
+ end
+ end
+end
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
index b29af732ad3..bc9686ed9cf 100644
--- a/spec/support/helpers/fake_blob_helpers.rb
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -1,6 +1,6 @@
module FakeBlobHelpers
class FakeBlob
- include Linguist::BlobHelper
+ include BlobLike
attr_reader :path, :size, :data, :lfs_oid, :lfs_size
@@ -19,10 +19,6 @@ module FakeBlobHelpers
alias_method :name, :path
- def mode
- nil
- end
-
def id
0
end
@@ -31,17 +27,11 @@ module FakeBlobHelpers
@binary
end
- def load_all_data!(repository)
- # No-op
+ def external_storage
+ :lfs if @lfs_pointer
end
- def lfs_pointer?
- @lfs_pointer
- end
-
- def truncated?
- false
- end
+ alias_method :external_size, :lfs_size
end
def fake_blob(**kwargs)