Commit fd1811d3 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into layout-nav-es-module

* master: (21 commits)
  Prevent some specs from mangling the gitlab-shell checkout
  Line up search dropdown with other nav dropdowns
  Fix onion-skin re-entering state
  Remove related links in MR widget when empty state
  Show inline edit button for issues
  Fix tags in the Activity tab not being clickable
  Fix shortcut links on help page
  Don't link LFS-objects multiple times.
  [CE->EE] Fix spec/lib/gitlab/git/gitlab_projects_spec.rb
  Tidy up the documentation of Gitlab HA/Gitlab Application
  Make sure two except won't overwrite each other
  Update axios.md
  Remove transitionend event from GL dropdown
  Preserve gem path so that we use the same gems
  Load commit in batches for pipelines#index
  BlobViewer::PackageJson - if private link to homepage
  Do not generate links for private NPM modules in blob view
  Remove block styling from search dropdown
  Fix sidebar height when performance bar enabled
  Remove all dropdown animations and set display: none if they're not open
  ...
parents 30ea58da febb0b9a
...@@ -80,10 +80,14 @@ stages: ...@@ -80,10 +80,14 @@ stages:
except: except:
- /(^qa[\/-].*|.*-qa$)/ - /(^qa[\/-].*|.*-qa$)/
.except-docs-and-qa: &except-docs-and-qa
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
.rspec-metadata: &rspec-metadata .rspec-metadata: &rspec-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
script: script:
...@@ -121,8 +125,7 @@ stages: ...@@ -121,8 +125,7 @@ stages:
.spinach-metadata: &spinach-metadata .spinach-metadata: &spinach-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
script: script:
...@@ -222,8 +225,7 @@ review-docs-cleanup: ...@@ -222,8 +225,7 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports # Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata: retrieve-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
stage: prepare stage: prepare
cache: cache:
key: tests_metadata key: tests_metadata
...@@ -378,8 +380,7 @@ spinach-mysql 3 4: *spinach-metadata-mysql ...@@ -378,8 +380,7 @@ spinach-mysql 3 4: *spinach-metadata-mysql
.rake-exec: &rake-exec .rake-exec: &rake-exec
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
<<: *ruby-static-analysis <<: *ruby-static-analysis
stage: test stage: test
...@@ -443,8 +444,7 @@ ee_compat_check: ...@@ -443,8 +444,7 @@ ee_compat_check:
# DB migration, rollback, and seed jobs # DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
script: script:
...@@ -466,8 +466,7 @@ db:check-schema-pg: ...@@ -466,8 +466,7 @@ db:check-schema-pg:
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
variables: variables:
...@@ -494,8 +493,7 @@ migration:path-mysql: ...@@ -494,8 +493,7 @@ migration:path-mysql:
.db-rollback: &db-rollback .db-rollback: &db-rollback
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
script: script:
...@@ -512,8 +510,7 @@ db:rollback-mysql: ...@@ -512,8 +510,7 @@ db:rollback-mysql:
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
variables: variables:
...@@ -541,8 +538,7 @@ db:seed_fu-mysql: ...@@ -541,8 +538,7 @@ db:seed_fu-mysql:
# Frontend-related jobs # Frontend-related jobs
gitlab:assets:compile: gitlab:assets:compile:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
dependencies: [] dependencies: []
...@@ -564,8 +560,7 @@ gitlab:assets:compile: ...@@ -564,8 +560,7 @@ gitlab:assets:compile:
karma: karma:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
<<: *use-pg <<: *use-pg
stage: test stage: test
...@@ -619,8 +614,7 @@ qa:internal: ...@@ -619,8 +614,7 @@ qa:internal:
coverage: coverage:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: post-test stage: post-test
services: [] services: []
...@@ -639,8 +633,7 @@ coverage: ...@@ -639,8 +633,7 @@ coverage:
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: post-test stage: post-test
dependencies: dependencies:
...@@ -699,8 +692,7 @@ cache gems: ...@@ -699,8 +692,7 @@ cache gems:
gitlab_git_test: gitlab_git_test:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
......
...@@ -263,7 +263,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0' ...@@ -263,7 +263,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader' gem 'batch-loader', '~> 1.2.1'
# Perf bar # Perf bar
gem 'peek', '~> 1.0.1' gem 'peek', '~> 1.0.1'
......
...@@ -78,7 +78,7 @@ GEM ...@@ -78,7 +78,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
batch-loader (1.1.1) batch-loader (1.2.1)
bcrypt (3.1.11) bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0) bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
...@@ -988,7 +988,7 @@ DEPENDENCIES ...@@ -988,7 +988,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
batch-loader batch-loader (~> 1.2.1)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
......
...@@ -176,6 +176,7 @@ export default class ImageFile { ...@@ -176,6 +176,7 @@ export default class ImageFile {
left: dragTrackWidth left: dragTrackWidth
}); });
$frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, function(e, left) { _this.initDraggable($dragger, framePadding, function(e, left) {
......
import Mousetrap from 'mousetrap';
function addMousetrapClick(el, key) {
el.addEventListener('click', () => Mousetrap.trigger(key));
}
function domContentLoaded() {
addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?');
addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
...@@ -300,7 +300,7 @@ GitLabDropdown = (function() { ...@@ -300,7 +300,7 @@ GitLabDropdown = (function() {
return function(data) { return function(data) {
_this.fullData = data; _this.fullData = data;
_this.parseData(_this.fullData); _this.parseData(_this.fullData);
_this.focusTextInput(true); _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input'); return _this.filter.input.trigger('input');
} }
...@@ -790,24 +790,16 @@ GitLabDropdown = (function() { ...@@ -790,24 +790,16 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking]; return [selectedObject, isMarking];
}; };
GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) { GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) { if (this.options.filterable) {
this.dropdown.one('transitionend', () => { const initialScrollTop = $(window).scrollTop();
const initialScrollTop = $(window).scrollTop();
if (this.dropdown.is('.open')) { if (this.dropdown.is('.open')) {
this.filterInput.focus(); this.filterInput.focus();
} }
if ($(window).scrollTop() < initialScrollTop) {
$(window).scrollTop(initialScrollTop);
}
});
if (triggerFocus) { if ($(window).scrollTop() < initialScrollTop) {
// This triggers after a ajax request $(window).scrollTop(initialScrollTop);
// in case of slow requests, the dropdown transition could already be finished
this.dropdown.trigger('transitionend');
} }
} }
}; };
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
showInlineEditButton: { showInlineEditButton: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: true,
}, },
showDeleteButton: { showDeleteButton: {
type: Boolean, type: Boolean,
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
v-tooltip v-tooltip
v-if="showInlineEditButton && canUpdate" v-if="showInlineEditButton && canUpdate"
type="button" type="button"
class="btn btn-default btn-edit btn-svg" class="btn btn-default btn-edit btn-svg js-issuable-edit"
v-html="pencilIcon" v-html="pencilIcon"
title="Edit title and description" title="Edit title and description"
data-placement="bottom" data-placement="bottom"
......
import Vue from 'vue'; import Vue from 'vue';
import eventHub from './event_hub';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
...@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data'); const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"')); const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.js-issuable-edit').on('click', (e) => {
e.preventDefault();
eventHub.$emit('open.form');
});
return new Vue({ return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
components: { components: {
......
...@@ -51,7 +51,10 @@ export default class Shortcuts { ...@@ -51,7 +51,10 @@ export default class Shortcuts {
} }
onToggleHelp(e) { onToggleHelp(e) {
e.preventDefault(); if (e.preventDefault) {
e.preventDefault();
}
Shortcuts.toggleHelp(this.enabledHelp); Shortcuts.toggleHelp(this.enabledHelp);
} }
...@@ -112,6 +115,9 @@ export default class Shortcuts { ...@@ -112,6 +115,9 @@ export default class Shortcuts {
static focusSearch(e) { static focusSearch(e) {
$('#search').focus(); $('#search').focus();
e.preventDefault();
if (e.preventDefault) {
e.preventDefault();
}
} }
} }
...@@ -62,7 +62,7 @@ export default { ...@@ -62,7 +62,7 @@ export default {
return this.mr.hasCI; return this.mr.hasCI;
}, },
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return !!this.mr.relatedLinks; return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
}, },
shouldRenderDeployments() { shouldRenderDeployments() {
return this.mr.deployments.length; return this.mr.deployments.length;
......
import { stateKey } from './state_maps';
export default function deviseState(data) { export default function deviseState(data) {
if (data.project_archived) { if (data.project_archived) {
return 'archived'; return stateKey.archived;
} else if (data.branch_missing) { } else if (data.branch_missing) {
return 'missingBranch'; return stateKey.missingBranch;
} else if (!data.commits_count) { } else if (!data.commits_count) {
return 'nothingToMerge'; return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked') { } else if (this.mergeStatus === 'unchecked') {
return 'checking'; return stateKey.checking;
} else if (data.has_conflicts) { } else if (data.has_conflicts) {
return 'conflicts'; return stateKey.conflicts;
} else if (data.work_in_progress) { } else if (data.work_in_progress) {
return 'workInProgress'; return stateKey.workInProgress;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return 'pipelineFailed'; return stateKey.pipelineFailed;
} else if (this.hasMergeableDiscussionsState) { } else if (this.hasMergeableDiscussionsState) {
return 'unresolvedDiscussions'; return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) { } else if (this.isPipelineBlocked) {
return 'pipelineBlocked'; return stateKey.pipelineBlocked;
} else if (this.hasSHAChanged) { } else if (this.hasSHAChanged) {
return 'shaMismatch'; return stateKey.shaMismatch;
} else if (this.mergeWhenPipelineSucceeds) { } else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds'; return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) { } else if (!this.canMerge) {
return 'notAllowedToMerge'; return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) { } else if (this.canBeMerged) {
return 'readyToMerge'; return stateKey.readyToMerge;
} }
return null; return null;
} }
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies'; import { getStateKey } from '../dependencies';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore { export default class MergeRequestStore {
...@@ -120,6 +121,10 @@ export default class MergeRequestStore { ...@@ -120,6 +121,10 @@ export default class MergeRequestStore {
} }
} }
get isNothingToMergeState() {
return this.state === stateKey.nothingToMerge;
}
static getEventObject(event) { static getEventObject(event) {
return { return {
author: MergeRequestStore.getAuthorObject(event), author: MergeRequestStore.getAuthorObject(event),
......
...@@ -31,6 +31,23 @@ const statesToShowHelpWidget = [ ...@@ -31,6 +31,23 @@ const statesToShowHelpWidget = [
'autoMergeFailed', 'autoMergeFailed',
]; ];
export const stateKey = {
archived: 'archived',
missingBranch: 'missingBranch',
nothingToMerge: 'nothingToMerge',
checking: 'checking',
conflicts: 'conflicts',
workInProgress: 'workInProgress',
pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked',
shaMismatch: 'shaMismatch',
autoMergeFailed: 'autoMergeFailed',
mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
};
export default { export default {
stateToComponentMap, stateToComponentMap,
statesToShowHelpWidget, statesToShowHelpWidget,
......
...@@ -9,12 +9,6 @@ ...@@ -9,12 +9,6 @@
padding-left: $contextual-sidebar-width; padding-left: $contextual-sidebar-width;
} }
// Override position: absolute
.right-sidebar {
position: fixed;
height: calc(100% - #{$header-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
padding: 10px 0 15px; padding: 10px 0 15px;
} }
......
...@@ -16,27 +16,18 @@ ...@@ -16,27 +16,18 @@
@mixin set-visible { @mixin set-visible {
transform: translateY(0); transform: translateY(0);
visibility: visible; display: block;
opacity: 1;
transition-duration: 100ms, 150ms, 25ms;
transition-delay: 35ms, 50ms, 25ms;
} }
@mixin set-invisible { @mixin set-invisible {
transform: translateY(-10px); transform: translateY(-10px);
visibility: hidden; display: none;
opacity: 0;
transition-property: opacity, transform, visibility;
transition-duration: 70ms, 250ms, 250ms;
transition-timing-function: linear, $dropdown-animation-timing;
transition-delay: 25ms, 50ms, 0ms;
} }
.open { .open {
.dropdown-menu, .dropdown-menu,
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-visible; @include set-visible;
display: block;
min-height: 40px; min-height: 40px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -55,6 +46,11 @@ ...@@ -55,6 +46,11 @@
} }
} }
// Get search dropdown to line up with other nav dropdowns
.search-input-container .dropdown-menu {
margin-top: 11px;
}
.dropdown-toggle { .dropdown-toggle {
padding: 6px 8px 6px 10px; padding: 6px 8px 6px 10px;
background-color: $white-light; background-color: $white-light;
...@@ -214,7 +210,6 @@ ...@@ -214,7 +210,6 @@
.dropdown-menu, .dropdown-menu,
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-invisible; @include set-invisible;
display: block;
position: absolute; position: absolute;
width: auto; width: auto;
top: 100%; top: 100%;
......
...@@ -90,11 +90,6 @@ ...@@ -90,11 +90,6 @@
.right-sidebar { .right-sidebar {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
height: calc(100% - #{$header-height}); height: calc(100% - #{$header-height});
&.affix {
position: fixed;
top: $header-height;
}
} }
.with-performance-bar .right-sidebar.affix { .with-performance-bar .right-sidebar.affix {
......
...@@ -122,7 +122,7 @@ ...@@ -122,7 +122,7 @@
} }
.right-sidebar { .right-sidebar {
position: absolute; position: fixed;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
...@@ -502,7 +502,7 @@ ...@@ -502,7 +502,7 @@
top: $header-height + $performance-bar-height; top: $header-height + $performance-bar-height;
.issuable-sidebar { .issuable-sidebar {
height: calc(100% - #{$header-height} - #{$performance-bar-height}); height: calc(100% - #{$performance-bar-height});
} }
} }
......
...@@ -108,13 +108,6 @@ input[type="checkbox"]:hover { ...@@ -108,13 +108,6 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning // Custom dropdown positioning
.dropdown-menu { .dropdown-menu {
transition-property: opacity, transform;
transition-duration: 250ms, 250ms;
transition-delay: 0ms, 25ms;
transition-timing-function: $dropdown-animation-timing;
transform: translateY(0);
opacity: 0;
display: block;
left: -5px; left: -5px;
} }
...@@ -152,13 +145,6 @@ input[type="checkbox"]:hover { ...@@ -152,13 +145,6 @@ input[type="checkbox"]:hover {
background-color: $nav-badge-bg; background-color: $nav-badge-bg;
border-color: $border-color; border-color: $border-color;
} }
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
transform: translateY(7px);
opacity: 1;
}
} }
&.has-value { &.has-value {
......
...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipelines_count = PipelinesFinder @pipelines_count = PipelinesFinder
.new(project).execute.count .new(project).execute.count
@pipelines.map(&:commit) # List commits for batch loading
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
......
...@@ -27,10 +27,17 @@ module BlobViewer ...@@ -27,10 +27,17 @@ module BlobViewer
private private
def package_name_from_json(key) def json_data
prepare! @json_data ||= begin
prepare!
JSON.parse(blob.data)
rescue
{}
end
end
JSON.parse(blob.data)[key] rescue nil def package_name_from_json(key)
json_data[key]
end end
def package_name_from_method_call(name) def package_name_from_method_call(name)
......
...@@ -16,7 +16,25 @@ module BlobViewer ...@@ -16,7 +16,25 @@ module BlobViewer
@package_name ||= package_name_from_json('name') @package_name ||= package_name_from_json('name')
end end
def package_type
private? ? 'private package' : super
end
def package_url def package_url
private? ? homepage : npm_url
end
private
def private?
!!json_data['private']
end
def homepage
json_data['homepage']
end
def npm_url
"https://www.npmjs.com/package/#{package_name}" "https://www.npmjs.com/package/#{package_name}"
end end
end end
......
...@@ -287,8 +287,12 @@ module Ci ...@@ -287,8 +287,12 @@ module Ci
Ci::Pipeline.truncate_sha(sha) Ci::Pipeline.truncate_sha(sha)
end end
# NOTE: This is loaded lazily and will never be nil, even if the commit
# cannot be found.
#
# Use constructs like: `pipeline.commit.present?`
def commit def commit
@commit ||= project.commit_by(oid: sha) @commit ||= Commit.lazy(project, sha)
end end
def branch? def branch?
...@@ -338,12 +342,9 @@ module Ci ...@@ -338,12 +342,9 @@ module Ci
end end
def latest? def latest?
return false unless ref return false unless ref && commit.present?
commit = project.commit(ref)
return false unless commit
commit.sha == sha project.commit(ref) == commit
end end
def retried def retried
......
...@@ -86,6 +86,20 @@ class Commit ...@@ -86,6 +86,20 @@ class Commit
def valid_hash?(key) def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key) !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end end
def lazy(project, oid)
BatchLoader.for({ project: project, oid: oid }).batch do |items, loader|
items_by_project = items.group_by { |i| i[:project] }
items_by_project.each do |project, commit_ids|
oids = commit_ids.map { |i| i[:oid] }
project.repository.commits_by(oids: oids).each do |commit|
loader.call({ project: commit.project, oid: commit.id }, commit) if commit
end
end
end
end
end end
attr_accessor :raw attr_accessor :raw
...@@ -103,7 +117,7 @@ class Commit ...@@ -103,7 +117,7 @@ class Commit
end end
def ==(other) def ==(other)
(self.class === other) && (raw == other.raw) other.is_a?(self.class) && raw == other.raw
end end
def self.reference_prefix def self.reference_prefix
...@@ -224,8 +238,8 @@ class Commit ...@@ -224,8 +238,8 @@ class Commit
notes.includes(:author) notes.includes(:author)
end end
def method_missing(m, *args, &block) def method_missing(method, *args, &block)
@raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end end
def respond_to_missing?(method, include_private = false) def respond_to_missing?(method, include_private = false)
......
...@@ -118,6 +118,18 @@ class Repository ...@@ -118,6 +118,18 @@ class Repository
@commit_cache[oid] = find_commit(oid) @commit_cache[oid] = find_commit(oid)
end end
def commits_by(oids:)
return [] unless oids.present?
commits = Gitlab::Git::Commit.batch_by_oid(raw_repository, oids)
if commits.present?
Commit.decorate(commits, @project)
else
[]
end
end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
options = { options = {
repo: raw_repository, repo: raw_repository,
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
if fork_source = @project.fork_source if fork_source = @project.fork_source
fork_source.lfs_objects.find_each do |lfs_object| fork_source.lfs_objects.find_each do |lfs_object|
lfs_object.projects << @project lfs_object.projects << @project unless lfs_object.projects.include?(@project)
end end
refresh_forks_count(fork_source) refresh_forks_count(fork_source)
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
%span.pushed #{event.action_name} #{event.ref_type} %span.pushed #{event.action_name} #{event.ref_type}
%strong %strong
- commits_link = project_commits_path(project, event.ref_name) - commits_link = project_commits_path(project, event.ref_name)
= link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link, class: 'ref-name' - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
= link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event = render "events/event_scope", event: event
......
= webpack_bundle_tag 'docs'
%div %div
- if current_application_settings.help_page_text.present? - if current_application_settings.help_page_text.present?
= markdown_field(current_application_settings, :help_page_text) = markdown_field(current_application_settings, :help_page_text)
...@@ -37,8 +39,12 @@ ...@@ -37,8 +39,12 @@
Quick help Quick help
%ul.well-list %ul.well-list
%li= link_to 'See our website for getting help', support_url %li= link_to 'See our website for getting help', support_url
%li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)' %li
%li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()' %button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
Use the search bar on the top of this page
%li
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
Use shortcuts
- unless current_application_settings.help_page_hide_commercial_content? - unless current_application_settings.help_page_hide_commercial_content?
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
...@@ -6,6 +6,6 @@ ...@@ -6,6 +6,6 @@
- if viewer.package_name - if viewer.package_name
and defines a #{viewer.package_type} named and defines a #{viewer.package_type} named
%strong< %strong<
= link_to viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer' = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer' = link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
...@@ -39,8 +39,6 @@ ...@@ -39,8 +39,6 @@
= icon('caret-down') = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
- if can_update_issue
%li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'js-issuable-edit'
- unless current_user == @issue.author - unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue - if can_update_issue
...@@ -52,9 +50,6 @@ ...@@ -52,9 +50,6 @@
%li.divider %li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- if can_update_issue
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped js-issuable-edit'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam - if can_report_spam
......
#js-pipeline-header-vue.pipeline-header-container #js-pipeline-header-vue.pipeline-header-container
- if @commit - if @commit.present?
.commit-box .commit-box
%h3.commit-title %h3.commit-title
= markdown(@commit.title, pipeline: :single_line) = markdown(@commit.title, pipeline: :single_line)
...@@ -8,28 +8,28 @@ ...@@ -8,28 +8,28 @@
%pre.commit-description %pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line)) = preserve(markdown(@commit.description, pipeline: :single_line))
.info-well .info-well
- if @commit.status - if @commit.status
.well-segment.pipeline-info .well-segment.pipeline-info
.icon-container .icon-container
= icon('clock-o') = icon('clock-o')
= pluralize @pipeline.total_size, "job" = pluralize @pipeline.total_size, "job"
- if @pipeline.ref - if @pipeline.ref
from from
= link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- if @pipeline.duration - if @pipeline.duration
in in
= time_interval_in_words(@pipeline.duration) = time_interval_in_words(@pipeline.duration)
- if @pipeline.queued_duration - if @pipeline.queued_duration
= "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
.well-segment.branch-info .well-segment.branch-info
.icon-container.commit-icon .icon-container.commit-icon
= custom_icon("icon_commit") = custom_icon("icon_commit")
= link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short" = link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
%span.text-expander %span.text-expander
\... \...
%span.js-details-content.hide %span.js-details-content.hide
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full" = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard") = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
...@@ -13,7 +13,7 @@ class ExpirePipelineCacheWorker ...@@ -13,7 +13,7 @@ class ExpirePipelineCacheWorker
store.touch(project_pipelines_path(project)) store.touch(project_pipelines_path(project))
store.touch(project_pipeline_path(project, pipeline)) store.touch(project_pipeline_path(project, pipeline))
store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil?
store.touch(new_merge_request_pipelines_path(project)) store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path| each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path) store.touch(path)
......
---
title: Fix tags in the Activity tab not being clickable
merge_request: 15996
author: Mario de la Ossa
type: fixed
---
title: Do not generate NPM links for private NPM modules in blob view
merge_request: 16002
author: Mario de la Ossa
type: added
---
title: Don't link LFS objects to a project when unlinking forks when they were already
linked
merge_request: 16006
author:
type: fixed
---
title: Fix shortcut links on help page
merge_request:
author:
type: fixed
---
title: Fix onion-skin re-entering state
merge_request:
author:
type: fixed
---
title: Remove related links in MR widget when empty state
merge_request:
author:
type: fixed
---
title: Move edit button to second row on issue page (and change it to a pencil icon)
merge_request:
author:
type: changed
...@@ -36,6 +36,7 @@ var config = { ...@@ -36,6 +36,7 @@ var config = {
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js',
deploy_keys: './deploy_keys/index.js', deploy_keys: './deploy_keys/index.js',
docs: './docs/docs_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js',
......
# Configuring GitLab for HA # Configuring GitLab for HA
Assuming you have already configured a database, Redis, and NFS, you can Assuming you have already configured a [database](database.md), [Redis](redis.md), and [NFS](nfs.md), you can
configure the GitLab application server(s) now. Complete the steps below configure the GitLab application server(s) now. Complete the steps below
for each GitLab application server in your environment. for each GitLab application server in your environment.
...@@ -48,34 +48,33 @@ for each GitLab application server in your environment. ...@@ -48,34 +48,33 @@ for each GitLab application server in your environment.
data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb` data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb`
configuration values for various scenarios. The example below assumes you've configuration values for various scenarios. The example below assumes you've
added NFS mounts in the default data locations. added NFS mounts in the default data locations.
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
# Prevent GitLab from starting if NFS data mounts are not available # Prevent GitLab from starting if NFS data mounts are not available
high_availability['mountpoint'] = '/var/opt/gitlab/git-data' high_availability['mountpoint'] = '/var/opt/gitlab/git-data'
# Disable components that will not be on the GitLab application server # Disable components that will not be on the GitLab application server
postgresql['enable'] = false roles ['application_role']
redis['enable'] = false
# PostgreSQL connection details # PostgreSQL connection details
gitlab_rails['db_adapter'] = 'postgresql' gitlab_rails['db_adapter'] = 'postgresql'
gitlab_rails['db_encoding'] = 'unicode' gitlab_rails['db_encoding'] = 'unicode'
gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server
gitlab_rails['db_password'] = 'DB password' gitlab_rails['db_password'] = 'DB password'
# Redis connection details # Redis connection details
gitlab_rails['redis_port'] = '6379' gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server
gitlab_rails['redis_password'] = 'Redis Password' gitlab_rails['redis_password'] = 'Redis Password'
``` ```
> **Note:** To maintain uniformity of links across HA clusters, the `external_url` > **Note:** To maintain uniformity of links across HA clusters, the `external_url`
on the first application server as well as the additional application on the first application server as well as the additional application
servers should point to the external url that users will use to access GitLab. servers should point to the external url that users will use to access GitLab.
In a typical HA setup, this will be the url of the load balancer which will In a typical HA setup, this will be the url of the load balancer which will
route traffic to all GitLab application servers in the HA cluster. route traffic to all GitLab application servers in the HA cluster.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration. 1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
......
...@@ -11,7 +11,7 @@ This exported module should be used instead of directly using `axios` to ensure ...@@ -11,7 +11,7 @@ This exported module should be used instead of directly using `axios` to ensure
## Usage ## Usage
```javascript ```javascript
import axios from '~/lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
axios.get(url) axios.get(url)
.then((response) => { .then((response) => {
......
...@@ -228,6 +228,19 @@ module Gitlab ...@@ -228,6 +228,19 @@ module Gitlab
end end
end end
end end
# Only to be used when the object ids will not necessarily have a
# relation to each other. The last 10 commits for a branch for example,
# should go through .where
def batch_by_oid(repo, oids)
repo.gitaly_migrate(:list_commits_by_oid) do |is_enabled|
if is_enabled
repo.gitaly_commit_client.list_commits_by_oid(oids)
else
oids.map { |oid| find(repo, oid) }.compact
end
end
end
end end
def initialize(repository, raw_commit, head = nil) def initialize(repository, raw_commit, head = nil)
......
...@@ -169,6 +169,15 @@ module Gitlab ...@@ -169,6 +169,15 @@ module Gitlab
consume_commits_response(response) consume_commits_response(response)
end end
def list_commits_by_oid(oids)
request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids)
response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
rescue GRPC::Unknown # If no repository is found, happens mainly during testing
[]
end
def commits_by_message(query, revision: '', path: '', limit: 1000, offset: 0) def commits_by_message(query, revision: '', path: '', limit: 1000, offset: 0)
request = Gitaly::CommitsByMessageRequest.new( request = Gitaly::CommitsByMessageRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
......
#!/usr/bin/env ruby #!/usr/bin/env ruby
gitaly_dir = 'tmp/tests/gitaly' gitaly_dir = 'tmp/tests/gitaly'
env = { 'HOME' => File.expand_path('tmp/tests') } env = { 'HOME' => File.expand_path('tmp/tests'),
'GEM_PATH' => Gem.path.join(':') }
args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml] args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml]
# Print the PID of the spawned process # Print the PID of the spawned process
......
...@@ -17,13 +17,10 @@ describe Projects::PipelinesController do ...@@ -17,13 +17,10 @@ describe Projects::PipelinesController do
describe 'GET index.json' do describe 'GET index.json' do
before do before do
branch_head = project.commit %w(pending running created success).each_with_index do |status, index|
parent = branch_head.parent sha = project.commit("HEAD~#{index}")
create(:ci_empty_pipeline, status: status, project: project, sha: sha)
create(:ci_empty_pipeline, status: 'pending', project: project, sha: branch_head.id) end
create(:ci_empty_pipeline, status: 'running', project: project, sha: branch_head.id)
create(:ci_empty_pipeline, status: 'created', project: project, sha: parent.id)
create(:ci_empty_pipeline, status: 'success', project: project, sha: parent.id)
end end
subject do subject do
...@@ -46,7 +43,7 @@ describe Projects::PipelinesController do ...@@ -46,7 +43,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do it 'limits the Gitaly requests' do
expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8) expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(3)
end end
end end
end end
......
...@@ -32,6 +32,24 @@ describe 'Help Pages' do ...@@ -32,6 +32,24 @@ describe 'Help Pages' do
it_behaves_like 'help page', prefix: '/gitlab' it_behaves_like 'help page', prefix: '/gitlab'
end end
context 'quick link shortcuts', :js do
before do
visit help_path
end
it 'focuses search bar' do
find('.js-trigger-search-bar').click
expect(page).to have_selector('#search:focus')
end
it 'opens shortcuts help dialog' do
find('.js-trigger-shortcut').click
expect(page).to have_selector('#modal-shortcuts')
end
end
end end
context 'in a production environment with version check enabled', :js do context 'in a production environment with version check enabled', :js do
......
...@@ -24,7 +24,7 @@ feature 'Issue Detail', :js do ...@@ -24,7 +24,7 @@ feature 'Issue Detail', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests wait_for_requests
click_link 'Edit' page.find('.js-issuable-edit').click
fill_in 'issuable-title', with: 'issue title' fill_in 'issuable-title', with: 'issue title'
click_button 'Save' click_button 'Save'
wait_for_requests wait_for_requests
......
...@@ -10,8 +10,6 @@ feature 'image diff notes', :js do ...@@ -10,8 +10,6 @@ feature 'image diff notes', :js do
project.team << [user, :master] project.team << [user, :master]
sign_in user sign_in user
page.driver.set_cookie('sidebar_collapsed', 'true')
# Stub helper to return any blob file as image from public app folder. # Stub helper to return any blob file as image from public app folder.
# This is necessary to run this specs since we don't display repo images in capybara. # This is necessary to run this specs since we don't display repo images in capybara.
allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png') allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png')
...@@ -141,13 +139,13 @@ feature 'image diff notes', :js do ...@@ -141,13 +139,13 @@ feature 'image diff notes', :js do
end end
it 'allows expanding/collapsing the discussion notes' do it 'allows expanding/collapsing the discussion notes' do
page.all('.js-diff-notes-toggle')[0].trigger('click') page.all('.js-diff-notes-toggle')[0].click
page.all('.js-diff-notes-toggle')[1].trigger('click') page.all('.js-diff-notes-toggle')[1].click
expect(page).not_to have_content('image diff test comment') expect(page).not_to have_content('image diff test comment')
page.all('.js-diff-notes-toggle')[0].trigger('click') page.all('.js-diff-notes-toggle')[0].click
page.all('.js-diff-notes-toggle')[1].trigger('click') page.all('.js-diff-notes-toggle')[1].click
expect(page).to have_content('image diff test comment') expect(page).to have_content('image diff test comment')
end end
...@@ -196,13 +194,31 @@ feature 'image diff notes', :js do ...@@ -196,13 +194,31 @@ feature 'image diff notes', :js do
expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;') expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
end end
it 'resets onion skin view mode opacity when toggling between view modes' do
find('.view-modes-menu .onion-skin').click
# Simulate dragging onion-skin slider
drag_and_drop_by(find('.dragger'), -30, 0)
expect(find('.onion-skin-frame .frame.added', visible: false)['style']).not_to match('opacity: 1;')
find('.view-modes-menu .swipe').click
find('.view-modes-menu .onion-skin').click
expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
end
end end
end
def create_image_diff_note def drag_and_drop_by(element, right_by, down_by)
find('.js-add-image-diff-note-button', match: :first).click page.driver.browser.action.drag_and_drop_by(element.native, right_by, down_by).perform
page.all('.js-add-image-diff-note-button')[0].trigger('click') end
find('.diff-content .note-textarea').native.send_keys('image diff test comment')
click_button 'Comment' def create_image_diff_note
wait_for_requests find('.js-add-image-diff-note-button', match: :first).click
page.all('.js-add-image-diff-note-button')[0].click
find('.diff-content .note-textarea').native.send_keys('image diff test comment')
click_button 'Comment'
wait_for_requests
end
end end
...@@ -2,15 +2,15 @@ require 'spec_helper' ...@@ -2,15 +2,15 @@ require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', :js do feature 'project owner sees a link to create a license file in empty project', :js do
let(:project_master) { create(:user) } let(:project_master) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project_empty_repo) }
background do background do
project.team << [project_master, :master] project.add_master(project_master)
sign_in(project_master) sign_in(project_master)
end end
scenario 'project master creates a license file from a template' do scenario 'project master creates a license file from a template' do
visit project_path(project) visit project_path(project)
click_link 'Create empty bare repository'
click_on 'LICENSE' click_on 'LICENSE'
expect(page).to have_content('New file') expect(page).to have_content('New file')
...@@ -26,8 +26,6 @@ feature 'project owner sees a link to create a license file in empty project', : ...@@ -26,8 +26,6 @@ feature 'project owner sees a link to create a license file in empty project', :
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true fill_in :commit_message, with: 'Add a LICENSE file', visible: true
# Remove pre-receive hook so we can push without auth
FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
click_button 'Commit changes' click_button 'Commit changes'
expect(current_path).to eq( expect(current_path).to eq(
......
...@@ -32,9 +32,7 @@ feature 'issuable templates', :js do ...@@ -32,9 +32,7 @@ feature 'issuable templates', :js do
message: 'added issue template', message: 'added issue template',
branch_name: 'master') branch_name: 'master')
visit project_issue_path project, issue visit project_issue_path project, issue
page.within('.js-issuable-actions') do page.find('.js-issuable-edit').click
click_on 'Edit'
end
fill_in :'issuable-title', with: 'test issue title' fill_in :'issuable-title', with: 'test issue title'
end end
...@@ -77,9 +75,7 @@ feature 'issuable templates', :js do ...@@ -77,9 +75,7 @@ feature 'issuable templates', :js do
message: 'added issue template', message: 'added issue template',
branch_name: 'master') branch_name: 'master')
visit project_issue_path project, issue visit project_issue_path project, issue
page.within('.js-issuable-actions') do page.find('.js-issuable-edit').click
click_on 'Edit'
end
fill_in :'issuable-title', with: 'test issue title' fill_in :'issuable-title', with: 'test issue title'
fill_in :'issue-description', with: prior_description fill_in :'issue-description', with: prior_description
end end
......
...@@ -4,18 +4,17 @@ feature 'Master views tags' do ...@@ -4,18 +4,17 @@ feature 'Master views tags' do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.team << [user, :master] project.add_master(user)
sign_in(user) sign_in(user)
end end
context 'when project has no tags' do context 'when project has no tags' do
let(:project) { create(:project_empty_repo) } let(:project) { create(:project_empty_repo) }
before do before do
visit project_path(project) visit project_path(project)
click_on 'README' click_on 'README'
fill_in :commit_message, with: 'Add a README file', visible: true fill_in :commit_message, with: 'Add a README file', visible: true
# Remove pre-receive hook so we can push without auth
FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
click_button 'Commit changes' click_button 'Commit changes'
visit project_tags_path(project) visit project_tags_path(project)
end end
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify'; import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from './mock_data'; import mockData from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../helpers/vue_mount_component_helper';
...@@ -344,4 +345,31 @@ describe('mrWidgetOptions', () => { ...@@ -344,4 +345,31 @@ describe('mrWidgetOptions', () => {
expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined(); expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
}); });
}); });
describe('rendering relatedLinks', () => {
beforeEach((done) => {
vm.mr.relatedLinks = {
assignToMe: null,
closing: `
<a class="close-related-link" href="#'>
Close
</a>
`,
mentioned: '',
};
Vue.nextTick(done);
});
it('renders if there are relatedLinks', () => {
expect(vm.$el.querySelector('.close-related-link')).toBeDefined();
});
it('does not render if state is nothingToMerge', (done) => {
vm.mr.state = stateKey.nothingToMerge;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.close-related-link')).toBeNull();
done();
});
});
});
}); });
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from '../mock_data'; import mockData from '../mock_data';
describe('MergeRequestStore', () => { describe('MergeRequestStore', () => {
...@@ -52,5 +53,17 @@ describe('MergeRequestStore', () => { ...@@ -52,5 +53,17 @@ describe('MergeRequestStore', () => {
expect(store.isPipelineSkipped).toBe(false); expect(store.isPipelineSkipped).toBe(false);
}); });
}); });
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
expect(store.isNothingToMergeState).toEqual(true);
});
it('returns false when not nothingToMerge', () => {
store.state = 'state';
expect(store.isNothingToMergeState).toEqual(false);
});
});
}); });
}); });
...@@ -41,7 +41,8 @@ describe Gitlab::Git::GitlabProjects do ...@@ -41,7 +41,8 @@ describe Gitlab::Git::GitlabProjects do
end end
it "fails if the source path doesn't exist" do it "fails if the source path doesn't exist" do
expect(logger).to receive(:error).with("mv-project failed: source path <#{tmp_repos_path}/bad-src.git> does not exist.") expected_source_path = File.join(tmp_repos_path, 'bad-src.git')
expect(logger).to receive(:error).with("mv-project failed: source path <#{expected_source_path}> does not exist.")
result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git') result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git')
expect(result).to be_falsy expect(result).to be_falsy
...@@ -50,7 +51,8 @@ describe Gitlab::Git::GitlabProjects do ...@@ -50,7 +51,8 @@ describe Gitlab::Git::GitlabProjects do
it 'fails if the destination path already exists' do it 'fails if the destination path already exists' do
FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git')) FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git'))
message = "mv-project failed: destination path <#{tmp_repos_path}/already-exists.git> already exists." expected_distination_path = File.join(tmp_repos_path, 'already-exists.git')
message = "mv-project failed: destination path <#{expected_distination_path}> already exists."
expect(logger).to receive(:error).with(message) expect(logger).to receive(:error).with(message)
expect(gl_projects.mv_project('already-exists.git')).to be_falsy expect(gl_projects.mv_project('already-exists.git')).to be_falsy
......
...@@ -22,4 +22,51 @@ describe BlobViewer::PackageJson do ...@@ -22,4 +22,51 @@ describe BlobViewer::PackageJson do
expect(subject.package_name).to eq('module-name') expect(subject.package_name).to eq('module-name')
end end
end end
describe '#package_url' do
it 'returns the package URL' do
expect(subject).to receive(:prepare!)
expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
end
end
describe '#package_type' do
it 'returns "package"' do
expect(subject).to receive(:prepare!)
expect(subject.package_type).to eq('package')
end
end
context 'when package.json has "private": true' do
let(:data) do
<<-SPEC.strip_heredoc
{
"name": "module-name",
"version": "10.3.1",
"private": true,
"homepage": "myawesomepackage.com"
}
SPEC
end
let(:blob) { fake_blob(path: 'package.json', data: data) }
subject { described_class.new(blob) }
describe '#package_url' do
it 'returns homepage if any' do
expect(subject).to receive(:prepare!)
expect(subject.package_url).to eq('myawesomepackage.com')
end
end
describe '#package_type' do
it 'returns "private package"' do
expect(subject).to receive(:prepare!)
expect(subject.package_type).to eq('private package')
end
end
end
end end
...@@ -13,6 +13,45 @@ describe Commit do ...@@ -13,6 +13,45 @@ describe Commit do
it { is_expected.to include_module(StaticModel) } it { is_expected.to include_module(StaticModel) }
end end
describe '.lazy' do
set(:project) { create(:project, :repository) }
context 'when the commits are found' do
let(:oids) do
%w(
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
)
end
subject { oids.map { |oid| described_class.lazy(project, oid) } }
it 'batches requests for commits' do
expect(project.repository).to receive(:commits_by).once.and_call_original
subject.first.title
subject.last.title
end
it 'maintains ordering' do
subject.each_with_index do |commit, i|
expect(commit.id).to eq(oids[i])
end
end
end
context 'when not found' do
it 'returns nil as commit' do
commit = described_class.lazy(project, 'deadbeef').__sync
expect(commit).to be_nil
end
end
end
describe '#author' do describe '#author' do
it 'looks up the author in a case-insensitive way' do it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase) user = create(:user, email: commit.author_email.upcase)
......
...@@ -239,6 +239,54 @@ describe Repository do ...@@ -239,6 +239,54 @@ describe Repository do
end end
end end
describe '#commits_by' do
set(:project) { create(:project, :repository) }
shared_examples 'batch commits fetching' do
let(:oids) { TestEnv::BRANCH_SHA.values }
subject { project.repository.commits_by(oids: oids) }
it 'finds each commit' do
expect(subject).not_to include(nil)
expect(subject.size).to eq(oids.size)
end
it 'returns only Commit instances' do
expect(subject).to all( be_a(Commit) )
end
context 'when some commits are not found ' do
let(:oids) do
['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10)
end
it 'returns only found commits' do
expect(subject).not_to include(nil)
expect(subject.size).to eq(10)
end
end
context 'when no oids are passed' do
let(:oids) { [] }
it 'does not call #batch_by_oid' do
expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid)
subject
end
end
end
context 'when Gitaly list_commits_by_oid is enabled' do
it_behaves_like 'batch commits fetching'
end
context 'when Gitaly list_commits_by_oid is enabled', :disable_gitaly do
it_behaves_like 'batch commits fetching'
end
end
describe '#find_commits_by_message' do describe '#find_commits_by_message' do
shared_examples 'finding commits by message' do shared_examples 'finding commits by message' do
it 'returns commits with messages containing a given string' do it 'returns commits with messages containing a given string' do
......
require 'spec_helper' require 'spec_helper'
describe PipelineSerializer do describe PipelineSerializer do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) } set(:user) { create(:user) }
let(:serializer) do let(:serializer) do
...@@ -16,7 +17,7 @@ describe PipelineSerializer do ...@@ -16,7 +17,7 @@ describe PipelineSerializer do
end end
context 'when a single object is being serialized' do context 'when a single object is being serialized' do
let(:resource) { create(:ci_empty_pipeline) } let(:resource) { create(:ci_empty_pipeline, project: project) }
it 'serializers the pipeline object' do it 'serializers the pipeline object' do
expect(subject[:id]).to eq resource.id expect(subject[:id]).to eq resource.id
...@@ -24,7 +25,7 @@ describe PipelineSerializer do ...@@ -24,7 +25,7 @@ describe PipelineSerializer do
end end
context 'when multiple objects are being serialized' do context 'when multiple objects are being serialized' do
let(:resource) { create_list(:ci_pipeline, 2) } let(:resource) { create_list(:ci_pipeline, 2, project: project) }
it 'serializers the array of pipelines' do it 'serializers the array of pipelines' do
expect(subject).not_to be_empty expect(subject).not_to be_empty
...@@ -100,7 +101,6 @@ describe PipelineSerializer do ...@@ -100,7 +101,6 @@ describe PipelineSerializer do
context 'number of queries' do context 'number of queries' do
let(:resource) { Ci::Pipeline.all } let(:resource) { Ci::Pipeline.all }
let(:project) { create(:project) }
before do before do
# Since RequestStore.active? is true we have to allow the # Since RequestStore.active? is true we have to allow the
......
...@@ -62,6 +62,26 @@ describe Projects::UnlinkForkService do ...@@ -62,6 +62,26 @@ describe Projects::UnlinkForkService do
expect(source.forks_count).to be_zero expect(source.forks_count).to be_zero
end end
context 'when the source has LFS objects' do
let(:lfs_object) { create(:lfs_object) }
before do
lfs_object.projects << project
end
it 'links the fork to the lfs object before unlinking' do
subject.execute
expect(lfs_object.projects).to include(forked_project)
end
it 'does not fail if the lfs objects were already linked' do
lfs_object.projects << forked_project
expect { subject.execute }.not_to raise_error
end
end
context 'when the original project was deleted' do context 'when the original project was deleted' do
it 'does not fail when the original project is deleted' do it 'does not fail when the original project is deleted' do
source = forked_project.forked_from_project source = forked_project.forked_from_project
......
require 'spec_helper'
describe 'events/event/_push.html.haml' do
let(:event) { build_stubbed(:push_event) }
context 'with a branch' do
let(:payload) { build_stubbed(:push_event_payload, event: event) }
before do
allow(event).to receive(:push_event_payload).and_return(payload)
end
it 'links to the branch' do
allow(event.project.repository).to receive(:branch_exists?).with(event.ref_name).and_return(true)
link = project_commits_path(event.project, event.ref_name)
render partial: 'events/event/push', locals: { event: event }
expect(rendered).to have_link(event.ref_name, href: link)
end
context 'that has been deleted' do
it 'does not link to the branch' do
render partial: 'events/event/push', locals: { event: event }
expect(rendered).not_to have_link(event.ref_name)
end
end
end
context 'with a tag' do
let(:payload) { build_stubbed(:push_event_payload, event: event, ref_type: :tag, ref: 'v0.1.0') }
before do
allow(event).to receive(:push_event_payload).and_return(payload)
end
it 'links to the tag' do
allow(event.project.repository).to receive(:tag_exists?).with(event.ref_name).and_return(true)
link = project_commits_path(event.project, event.ref_name)
render partial: 'events/event/push', locals: { event: event }
expect(rendered).to have_link(event.ref_name, href: link)
end
context 'that has been deleted' do
it 'does not link to the tag' do
render partial: 'events/event/push', locals: { event: event }
expect(rendered).not_to have_link(event.ref_name)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment