Commit 98a4aca6 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 8998_skip_pending_commits_if_not_head

* upstream/master:
  Show CI status as Favicon on Pipelines, Job and MR pages
  STL file viewer
  Wait for the PDF to be loaded before doing anything
  remove unnecessary lease as cron job
  Search for opened MRs - include reopened  MRs
  ProjectsFinder should handle more options
  Clearly show who triggered the pipeline in email
  Make it possible to preview pipeline success/failed emails
  Add remove_concurrent_index to database helper
  fix project authorizations migration issue
  attempt to fix migration
  Revert schema.rb
  attempt to fix db failure
  Periodically mark projects that are stuck in importing as failed
  Fix html structure to prevent tooltip from not hidding
  Enable creation of deploy keys with write access via the API
parents 06b4ea24 d062af91
import * as THREE from 'three/build/three.module';
import STLLoaderClass from 'three-stl-loader';
import OrbitControlsClass from 'three-orbit-controls';
import MeshObject from './mesh_object';
const STLLoader = STLLoaderClass(THREE);
const OrbitControls = OrbitControlsClass(THREE);
export default class Renderer {
constructor(container) {
this.renderWrapper = this.render.bind(this);
this.objects = [];
this.container = container;
this.width = this.container.offsetWidth;
this.height = 500;
this.loader = new STLLoader();
this.fov = 45;
this.camera = new THREE.PerspectiveCamera(
this.fov,
this.width / this.height,
1,
1000,
);
this.scene = new THREE.Scene();
this.scene.add(this.camera);
// Setup the viewer
this.setupRenderer();
this.setupGrid();
this.setupLight();
// Setup OrbitControls
this.controls = new OrbitControls(
this.camera,
this.renderer.domElement,
);
this.controls.minDistance = 5;
this.controls.maxDistance = 30;
this.controls.enableKeys = false;
this.loadFile();
}
setupRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true,
});
this.renderer.setClearColor(0xFFFFFF);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(
this.width,
this.height,
);
}
setupLight() {
// Point light illuminates the object
const pointLight = new THREE.PointLight(
0xFFFFFF,
2,
0,
);
pointLight.castShadow = true;
this.camera.add(pointLight);
// Ambient light illuminates the scene
const ambientLight = new THREE.AmbientLight(
0xFFFFFF,
1,
);
this.scene.add(ambientLight);
}
setupGrid() {
this.grid = new THREE.GridHelper(
20,
20,
0x000000,
0x000000,
);
this.scene.add(this.grid);
}
loadFile() {
this.loader.load(this.container.dataset.endpoint, (geo) => {
const obj = new MeshObject(geo);
this.objects.push(obj);
this.scene.add(obj);
this.start();
this.setDefaultCameraPosition();
});
}
start() {
// Empty the container first
this.container.innerHTML = '';
// Add to DOM
this.container.appendChild(this.renderer.domElement);
// Make controls visible
this.container.parentNode.classList.remove('is-stl-loading');
this.render();
}
render() {
this.renderer.render(
this.scene,
this.camera,
);
requestAnimationFrame(this.renderWrapper);
}
changeObjectMaterials(type) {
this.objects.forEach((obj) => {
obj.changeMaterial(type);
});
}
setDefaultCameraPosition() {
const obj = this.objects[0];
const radius = (obj.geometry.boundingSphere.radius / 1.5);
const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2));
this.camera.position.set(
0,
dist + 1,
dist,
);
this.camera.lookAt(this.grid);
this.controls.update();
}
}
import {
Matrix4,
MeshLambertMaterial,
Mesh,
} from 'three/build/three.module';
const defaultColor = 0xE24329;
const materials = {
default: new MeshLambertMaterial({
color: defaultColor,
}),
wireframe: new MeshLambertMaterial({
color: defaultColor,
wireframe: true,
}),
};
export default class MeshObject extends Mesh {
constructor(geo) {
super(
geo,
materials.default,
);
this.geometry.computeBoundingSphere();
this.rotation.set(-Math.PI / 2, 0, 0);
if (this.geometry.boundingSphere.radius > 4) {
const scale = 4 / this.geometry.boundingSphere.radius;
this.geometry.applyMatrix(
new Matrix4().makeScale(
scale,
scale,
scale,
),
);
this.geometry.computeBoundingSphere();
this.position.x = -this.geometry.boundingSphere.center.x;
this.position.z = this.geometry.boundingSphere.center.y;
}
}
changeMaterial(type) {
this.material = materials[type];
}
}
...@@ -10,7 +10,7 @@ Vue.use(PDFLab, { ...@@ -10,7 +10,7 @@ Vue.use(PDFLab, {
export default () => { export default () => {
const el = document.getElementById('js-pdf-viewer'); const el = document.getElementById('js-pdf-viewer');
new Vue({ return new Vue({
el, el,
data() { data() {
return { return {
......
import Renderer from './3d_viewer';
document.addEventListener('DOMContentLoaded', () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
const target = e.target;
e.preventDefault();
document.querySelector('.js-material-changer.active').classList.remove('active');
target.classList.add('active');
target.blur();
viewer.changeObjectMaterials(target.dataset.type);
});
});
});
...@@ -88,6 +88,7 @@ window.Build = (function() { ...@@ -88,6 +88,7 @@ window.Build = (function() {
dataType: 'json', dataType: 'json',
success: function(buildData) { success: function(buildData) {
$('.js-build-output').html(buildData.trace_html); $('.js-build-output').html(buildData.trace_html);
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) { if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height()); $("html,body").scrollTop(this.$buildTrace.height());
} }
......
...@@ -226,9 +226,11 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -226,9 +226,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:show': case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({ new gl.Pipelines({
initTabs: true, initTabs: true,
pipelineStatusUrl,
tabsOptions: { tabsOptions: {
action: controllerAction, action: controllerAction,
defaultAction: 'pipelines', defaultAction: 'pipelines',
......
...@@ -75,6 +75,7 @@ export default { ...@@ -75,6 +75,7 @@ export default {
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
aria-hidden="true"/> aria-hidden="true"/>
</span> </span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li v-for="action in actions">
...@@ -91,7 +92,6 @@ export default { ...@@ -91,7 +92,6 @@ export default {
</button> </button>
</li> </li>
</ul> </ul>
</button>
</div> </div>
`, `,
}; };
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
(function() { (function() {
(function(w) { (function(w) {
var base; var base;
const faviconEl = document.getElementById('favicon');
const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
w.gl || (w.gl = {}); w.gl || (w.gl = {});
(base = w.gl).utils || (base.utils = {}); (base = w.gl).utils || (base.utils = {});
w.gl.utils.isInGroupsPage = function() { w.gl.utils.isInGroupsPage = function() {
...@@ -361,5 +363,34 @@ ...@@ -361,5 +363,34 @@
fn(next, stop); fn(next, stop);
}); });
}; };
w.gl.utils.setFavicon = (iconName) => {
if (faviconEl && iconName) {
faviconEl.setAttribute('href', `/assets/${iconName}.ico`);
}
};
w.gl.utils.resetFavicon = () => {
if (faviconEl) {
faviconEl.setAttribute('href', originalFavicon);
}
};
w.gl.utils.setCiStatusFavicon = (pageUrl) => {
$.ajax({
url: pageUrl,
dataType: 'json',
success: function(data) {
if (data && data.icon) {
gl.utils.setFavicon(`ci_favicons/${data.icon}`);
} else {
gl.utils.resetFavicon();
}
},
error: function() {
gl.utils.resetFavicon();
}
});
};
})(window); })(window);
}).call(window); }).call(window);
...@@ -38,11 +38,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -38,11 +38,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
function MergeRequestWidget(opts) { function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior // Initialize MergeRequestWidget behavior
// //
// check_enable - Boolean, whether to check automerge status // check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use to check automerge status // merge_check_url - String, URL to use to check automerge status
// ci_status_url - String, URL to use to check CI status // ci_status_url - String, URL to use to check CI status
// pipeline_status_url - String, URL to use to get CI status for Favicon
// //
this.opts = opts; this.opts = opts;
this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`;
this.$widgetBody = $('.mr-widget-body'); this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({ $('#modal_merge_info').modal({
show: false show: false
...@@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.status = data.status; _this.status = data.status;
_this.hasCi = data.has_ci; _this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi); _this.updateMergeButton(_this.status, _this.hasCi);
gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url);
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status || if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha || data.sha !== _this.opts.ci_sha ||
......
...@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs'); ...@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs');
new global.LinkedTabs(options.tabsOptions); new global.LinkedTabs(options.tabsOptions);
} }
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns(); this.addMarginToBuildColumns();
} }
......
...@@ -275,3 +275,9 @@ span.idiff { ...@@ -275,3 +275,9 @@ span.idiff {
} }
} }
} }
.is-stl-loading {
.stl-controls {
display: none;
}
}
...@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer] before_action :group, only: [:show, :transfer]
def index def index
params[:sort] ||= 'latest_activity_desc'
@projects = Project.with_statistics @projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
......
# == FilterProjects
#
# Controller concern to handle projects filtering
# * by name
# * by archived state
#
module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
projects = projects.search(params[:name]) if params[:name].present?
projects = projects.non_archived if params[:archived].blank?
projects = projects.personal(current_user) if params[:personal].present? && current_user
projects
end
end
module ParamsBackwardCompatibility
private
def set_non_archived_param
params[:non_archived] = params[:archived].blank?
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects include ParamsBackwardCompatibility
before_action :set_non_archived_param
before_action :default_sorting
def index def index
@projects = load_projects(current_user.authorized_projects) @projects = load_projects(params.merge(non_public: true)).page(params[:page])
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html { @last_push = current_user.recent_push } format.html { @last_push = current_user.recent_push }
...@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def starred def starred
@projects = load_projects(current_user.viewable_starred_projects) @projects = load_projects(params.merge(starred: true)).
@projects = @projects.includes(:forked_from_project, :tags) includes(:forked_from_project, :tags).page(params[:page])
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@last_push = current_user.recent_push @last_push = current_user.recent_push
@groups = [] @groups = []
...@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private private
def load_projects(base_scope) def default_sorting
projects = base_scope.sorted_by_activity.includes(:route, namespace: :route) params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
end
filter_projects(projects) def load_projects(finder_params)
ProjectsFinder.new(params: finder_params, current_user: current_user).
execute.includes(:route, namespace: :route)
end end
def load_events def load_events
@events = Event.in_projects(load_projects(current_user.authorized_projects)) @events = Event.in_projects(load_projects(params.merge(non_public: true)))
@events = event_filter.apply_filter(@events).with_associations @events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
......
class Explore::ProjectsController < Explore::ApplicationController class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects include ParamsBackwardCompatibility
before_action :set_non_archived_param
def index def index
@projects = load_projects params[:sort] ||= 'latest_activity_desc'
@tags = @projects.tags_on(:tags) @sort = params[:sort]
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = load_projects.page(params[:page])
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = load_projects(Project.trending) params[:trending] = true
@projects = filter_projects(@projects) @sort = params[:sort]
@projects = @projects.sort(@sort = params[:sort]) @projects = load_projects.page(params[:page])
@projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def starred def starred
@projects = load_projects @projects = load_projects.reorder('star_count DESC').page(params[:page])
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
end end
protected private
def load_projects(base_scope = nil) def load_projects
base_scope ||= ProjectsFinder.new.execute(current_user) ProjectsFinder.new(current_user: current_user, params: params).
base_scope.includes(:route, namespace: :route) execute.includes(:route, namespace: :route)
end end
end end
...@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController ...@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController
end end
def group_projects def group_projects
@projects ||= GroupProjectsFinder.new(group).execute(current_user) @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end end
def authorize_admin_group! def authorize_admin_group!
......
class GroupsController < Groups::ApplicationController class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
include ParamsBackwardCompatibility
respond_to :html respond_to :html
...@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController ...@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController
protected protected
def setup_projects def setup_projects
set_non_archived_param
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
options = {} options = {}
options[:only_owned] = true if params[:shared] == '0' options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1' options[:only_shared] = true if params[:shared] == '1'
@projects = GroupProjectsFinder.new(group, options).execute(current_user) @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:name].blank? @projects = @projects.page(params[:page]) if params[:name].blank?
end end
......
...@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
def index def index
base_query = project.forks.includes(:creator) base_query = project.forks.includes(:creator)
@forks = base_query.merge(ProjectsFinder.new.execute(current_user)) @forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
@total_forks_count = base_query.size @total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size @private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count @public_forks_count = @total_forks_count - @private_forks_count
......
...@@ -140,6 +140,6 @@ class UsersController < ApplicationController ...@@ -140,6 +140,6 @@ class UsersController < ApplicationController
end end
def projects_for_current_user def projects_for_current_user
ProjectsFinder.new.execute(current_user) ProjectsFinder.new(current_user: current_user).execute
end end
end end
class GroupProjectsFinder < UnionFinder # GroupProjectsFinder
def initialize(group, options = {}) #
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# group
# options:
# only_owned: boolean
# only_shared: boolean
# params:
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class GroupProjectsFinder < ProjectsFinder
attr_reader :group, :options
def initialize(group:, params: {}, options: {}, current_user: nil, project_ids_relation: nil)
super(params: params, current_user: current_user, project_ids_relation: project_ids_relation)
@group = group @group = group
@options = options @options = options
end end
def execute(current_user = nil)
segments = group_projects(current_user)
find_union(segments, Project)
end
private private
def group_projects(current_user) def init_collection
only_owned = @options.fetch(:only_owned, false) only_owned = options.fetch(:only_owned, false)
only_shared = @options.fetch(:only_shared, false) only_shared = options.fetch(:only_shared, false)
projects = [] projects = []
if current_user if current_user
if @group.users.include?(current_user) if group.users.include?(current_user)
projects << @group.projects unless only_shared projects << group.projects unless only_shared
projects << @group.shared_projects unless only_owned projects << group.shared_projects unless only_owned
else else
unless only_shared unless only_shared
projects << @group.projects.visible_to_user(current_user) projects << group.projects.visible_to_user(current_user)
projects << @group.projects.public_to_user(current_user) projects << group.projects.public_to_user(current_user)
end end
unless only_owned unless only_owned
projects << @group.shared_projects.visible_to_user(current_user) projects << group.shared_projects.visible_to_user(current_user)
projects << @group.shared_projects.public_to_user(current_user) projects << group.shared_projects.public_to_user(current_user)
end end
end end
else else
projects << @group.projects.public_only unless only_shared projects << group.projects.public_only unless only_shared
projects << @group.shared_projects.public_only unless only_owned projects << group.shared_projects.public_only unless only_owned
end end
projects projects
end end
def union(items)
find_union(items, Project)
end
end end
...@@ -116,9 +116,9 @@ class IssuableFinder ...@@ -116,9 +116,9 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related? if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects current_user.authorized_projects
elsif group elsif group
GroupProjectsFinder.new(group).execute(current_user) GroupProjectsFinder.new(group: group, current_user: current_user).execute
else else
projects_finder.execute(current_user, item_project_ids(items)) ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
...@@ -405,8 +405,4 @@ class IssuableFinder ...@@ -405,8 +405,4 @@ class IssuableFinder
def current_user_related? def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end end
def projects_finder
@projects_finder ||= ProjectsFinder.new
end
end end
...@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder ...@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder
def projects def projects
return @projects if defined?(@projects) return @projects if defined?(@projects)
@projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user) @projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute
@projects = @projects.in_namespace(params[:group_id]) if group? @projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil) @projects = @projects.reorder(nil)
......
# ProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# params:
# trending: boolean
# non_public: boolean
# starred: boolean
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
def execute(current_user = nil, project_ids_relation = nil) attr_accessor :params
segments = all_projects(current_user) attr_reader :current_user, :project_ids_relation
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
find_union(segments, Project).with_route def initialize(params: {}, current_user: nil, project_ids_relation: nil)
@params = params
@current_user = current_user
@project_ids_relation = project_ids_relation
end
def execute
items = init_collection
items = by_ids(items)
items = union(items)
items = by_personal(items)
items = by_visibilty_level(items)
items = by_tags(items)
items = by_search(items)
items = by_archived(items)
sort(items)
end end
private private
def all_projects(current_user) def init_collection
projects = [] projects = []
projects << current_user.authorized_projects if current_user if params[:trending].present?
projects << Project.unscoped.public_to_user(current_user) projects << Project.trending
elsif params[:starred].present? && current_user
projects << current_user.viewable_starred_projects
else
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
end
projects projects
end end
def by_ids(items)
project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items
end
def union(items)
find_union(items, Project).with_route
end
def by_personal(items)
(params[:personal].present? && current_user) ? items.personal(current_user) : items
end
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
def by_tags(items)
params[:tag].present? ? items.tagged_with(params[:tag]) : items
end
def by_search(items)
params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items
end
def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items
end
def by_archived(projects)
# Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived)
params[:non_archived] ? projects.non_archived : projects
end
end end
...@@ -95,7 +95,7 @@ class TodosFinder ...@@ -95,7 +95,7 @@ class TodosFinder
def projects(items) def projects(items)
item_project_ids = items.reorder(nil).select(:project_id) item_project_ids = items.reorder(nil).select(:project_id)
ProjectsFinder.new.execute(current_user, item_project_ids) ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute
end end
def type? def type?
......
...@@ -25,8 +25,8 @@ module SortingHelper ...@@ -25,8 +25,8 @@ module SortingHelper
def projects_sort_options_hash def projects_sort_options_hash
options = { options = {
sort_value_name => sort_title_name, sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated, sort_value_latest_activity => sort_title_latest_activity,
sort_value_oldest_updated => sort_title_oldest_updated, sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_recently_created => sort_title_recently_created, sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created sort_value_oldest_created => sort_title_oldest_created
} }
...@@ -78,6 +78,14 @@ module SortingHelper ...@@ -78,6 +78,14 @@ module SortingHelper
'Last updated' 'Last updated'
end end
def sort_title_oldest_activity
'Oldest updated'
end
def sort_title_latest_activity
'Last updated'
end
def sort_title_oldest_created def sort_title_oldest_created
'Oldest created' 'Oldest created'
end end
...@@ -198,6 +206,14 @@ module SortingHelper ...@@ -198,6 +206,14 @@ module SortingHelper
'updated_desc' 'updated_desc'
end end
def sort_value_oldest_activity
'latest_activity_asc'
end
def sort_value_latest_activity
'latest_activity_desc'
end
def sort_value_oldest_created def sort_value_oldest_created
'created_asc' 'created_asc'
end end
......
...@@ -58,6 +58,10 @@ class Blob < SimpleDelegator ...@@ -58,6 +58,10 @@ class Blob < SimpleDelegator
binary? && extname.downcase.delete('.') == 'sketch' binary? && extname.downcase.delete('.') == 'sketch'
end end
def stl?
extname.downcase.delete('.') == 'stl'
end
def size_within_svg_limits? def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE size <= MAXIMUM_SVG_SIZE
end end
...@@ -81,6 +85,8 @@ class Blob < SimpleDelegator ...@@ -81,6 +85,8 @@ class Blob < SimpleDelegator
'notebook' 'notebook'
elsif sketch? elsif sketch?
'sketch' 'sketch'
elsif stl?
'stl'
elsif text? elsif text?
'text' 'text'
else else
......
...@@ -351,10 +351,15 @@ class Project < ActiveRecord::Base ...@@ -351,10 +351,15 @@ class Project < ActiveRecord::Base
end end
def sort(method) def sort(method)
if method == 'storage_size_desc' case method.to_s
when 'storage_size_desc'
# storage_size is a joined column so we need to # storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name # pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC') reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc'
reorder(last_activity_at: :desc)
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
else else
order_by(method) order_by(method)
end end
......
...@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy ...@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy
can_read ||= globally_viewable can_read ||= globally_viewable
can_read ||= member can_read ||= member
can_read ||= @user.admin? can_read ||= @user.admin?
can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any? can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read can! :read_group if can_read
# Only group masters and group owners can create new projects # Only group masters and group owners can create new projects
...@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy ...@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy
return true if @subject.internal? && !@user.external? return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user) return true if @subject.users.include?(@user)
GroupProjectsFinder.new(@subject).execute(@user).any? GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end end
end end
...@@ -39,7 +39,7 @@ module MergeRequests ...@@ -39,7 +39,7 @@ module MergeRequests
private private
# Returns all origin and fork merge requests from `@project` satisfying passed arguments. # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
def merge_requests_for(source_branch, mr_states: [:opened]) def merge_requests_for(source_branch, mr_states: [:opened, :reopened])
MergeRequest MergeRequest
.with_state(mr_states) .with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id) .where(source_branch: source_branch, source_project_id: @project.id)
......
...@@ -8,7 +8,7 @@ module Search ...@@ -8,7 +8,7 @@ module Search
def execute def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present? group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user) projects = ProjectsFinder.new(current_user: current_user).execute
if group if group
projects = projects.inside_path(group.full_path) projects = projects.inside_path(group.full_path)
......
- publicish_project_count = ProjectsFinder.new.execute(current_user).count - publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count
.blank-state.blank-state-welcome .blank-state.blank-state-welcome
%h2.blank-state-welcome-title %h2.blank-state-welcome-title
Welcome to GitLab Welcome to GitLab
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
%title= page_title(site_name) %title= page_title(site_name)
%meta{ name: "description", content: page_description } %meta{ name: "description", content: page_description }
= favicon_link_tag favicon = favicon_link_tag favicon, id: 'favicon'
= stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "print", media: "print"
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;line-height:1;" }
%img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ %img{ alt: "", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has failed. Your pipeline has failed.
%tr.spacer %tr.spacer
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= @project.name = @project.name
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
= @pipeline.ref = @pipeline.ref
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
...@@ -52,13 +52,13 @@ ...@@ -52,13 +52,13 @@
= @merge_request.to_reference = @merge_request.to_reference
.commit{ style: "color:#5c5c5c;font-weight:300;" } .commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50) = @pipeline.git_commit_message.truncate(50)
- commit = @pipeline.commit
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
- commit = @pipeline.commit
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
...@@ -68,15 +68,48 @@ ...@@ -68,15 +68,48 @@
- else - else
%span %span
= commit.author_name = commit.author_name
- if commit.different_committer?
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
= commit.committer.name
- else
%span
= commit.committer_name
%tr.spacer %tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp; &nbsp;
- failed = @pipeline.statuses.latest.failed
%tr.pre-section %tr.pre-section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" }
Pipeline %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } %tbody
= "\##{@pipeline.id}" %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
Pipeline
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
- else
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API
- failed = @pipeline.statuses.latest.failed
%tr
%td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
had had
= failed.size = failed.size
failed failed
...@@ -94,8 +127,8 @@ ...@@ -94,8 +127,8 @@
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" }
%img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ %img{ alt: "", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
= build.stage = build.stage
%td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
......
...@@ -14,9 +14,21 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) ...@@ -14,9 +14,21 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
<% else -%> <% else -%>
Commit Author: <%= commit.author_name %> Commit Author: <%= commit.author_name %>
<% end -%> <% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
<% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
<% failed = @pipeline.statuses.latest.failed -%> <% failed = @pipeline.statuses.latest.failed -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%> <% failed.each do |build| -%>
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %> <%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= @project.name = @project.name
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
= @pipeline.ref = @pipeline.ref
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
...@@ -52,13 +52,13 @@ ...@@ -52,13 +52,13 @@
= @merge_request.to_reference = @merge_request.to_reference
.commit{ style: "color:#5c5c5c;font-weight:300;" } .commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50) = @pipeline.git_commit_message.truncate(50)
- commit = @pipeline.commit
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody %tbody
%tr %tr
- commit = @pipeline.commit
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
...@@ -68,17 +68,50 @@ ...@@ -68,17 +68,50 @@
- else - else
%span %span
= commit.author_name = commit.author_name
- if commit.different_committer?
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
= commit.committer.name
- else
%span
= commit.committer_name
%tr.spacer %tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp; &nbsp;
%tr.success-message %tr.success-message
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" }
- build_count = @pipeline.statuses.latest.size %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
Pipeline
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
- else
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API
%tr
%td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
- job_count = @pipeline.statuses.latest.size
- stage_count = @pipeline.stages_count - stage_count = @pipeline.stages_count
Pipeline
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
successfully completed successfully completed
#{build_count} #{'build'.pluralize(build_count)} #{job_count} #{'job'.pluralize(job_count)}
in in
#{stage_count} #{'stage'.pluralize(stage_count)}. #{stage_count} #{'stage'.pluralize(stage_count)}.
...@@ -14,7 +14,19 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) ...@@ -14,7 +14,19 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
<% else -%> <% else -%>
Commit Author: <%= commit.author_name %> Commit Author: <%= commit.author_name %>
<% end -%> <% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
<% build_count = @pipeline.statuses.latest.size -%> <% build_count = @pipeline.statuses.latest.size -%>
<% stage_count = @pipeline.stages_count -%> <% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. <% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('stl_viewer')
.file-content.is-stl-loading
.text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
.text-center.prepend-top-default.append-bottom-default.stl-controls
.btn-group
%button.btn.btn-default.btn-sm.js-material-changer{ data: { type: 'wireframe' } }
Wireframe
%button.btn.btn-default.btn-sm.active.js-material-changer{ data: { type: 'default' } }
Solid
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"}, check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
pipeline_status_url: "#{pipeline_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
......
- @sort ||= sort_value_recently_updated - @sort ||= sort_value_latest_activity
.dropdown .dropdown
- toggle_text = projects_sort_options_hash[@sort] - toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
......
...@@ -2,6 +2,8 @@ class RepositoryImportWorker ...@@ -2,6 +2,8 @@ class RepositoryImportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION
attr_accessor :project, :current_user attr_accessor :project, :current_user
def perform(project_id) def perform(project_id)
...@@ -12,7 +14,7 @@ class RepositoryImportWorker ...@@ -12,7 +14,7 @@ class RepositoryImportWorker
import_url: @project.import_url, import_url: @project.import_url,
path: @project.path_with_namespace) path: @project.path_with_namespace)
project.update_column(:import_error, nil) project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute result = Projects::ImportService.new(project, current_user).execute
......
class StuckImportJobsWorker
include Sidekiq::Worker
include CronjobQueue
IMPORT_EXPIRATION = 15.hours.to_i
def perform
stuck_projects.find_in_batches(batch_size: 500) do |group|
jids = group.map(&:import_jid)
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids)
if completed_jids.any?
completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id)
fail_batch!(completed_jids, completed_ids)
end
end
end
private
def stuck_projects
Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil)
end
def fail_batch!(completed_jids, completed_ids)
Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message)
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}")
end
def error_message
"Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds"
end
end
---
title: Show CI status as Favicon on Pipelines, Job and MR pages
merge_request: 10144
author:
---
title: ProjectsFinder should handle more options
merge_request: 9682
author: Jacopo Beschi @jacopo-beschi
---
title: Enable creation of deploy keys with write access via the API
merge_request:
author:
---
title: Include reopened MRs when searching for opened ones
merge_request: 10407
author:
---
title: Fixes HTML structure that was preventing the tooltip to disappear when hovering
out of the button.
merge_request:
author:
---
title: Add remove_concurrent_index to database helper
merge_request: 10441
author: blackst0ne
---
title: Periodically mark projects that are stuck in importing as failed
merge_request:
author:
---
title: Clearly show who triggered the pipeline in email
merge_request: 10283
author:
...@@ -349,6 +349,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW ...@@ -349,6 +349,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *' Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker' Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker'
# #
# GitLab Shell # GitLab Shell
......
...@@ -42,6 +42,7 @@ var config = { ...@@ -42,6 +42,7 @@ var config = {
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'], u2f: ['vendor/u2f'],
users: './users/users_bundle.js', users: './users/users_bundle.js',
......
# rubocop:disable RemoveIndex
class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexOnRunnersLocked < ActiveRecord::Migration class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForPipelineUserId < ActiveRecord::Migration class AddIndexForPipelineUserId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class MergeRequestDiffRemoveUniq < ActiveRecord::Migration class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# rubocop:disable RemoveIndex
class MergeRequestDiffAddIndex < ActiveRecord::Migration class MergeRequestDiffAddIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# rubocop:disable RemoveIndex
class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToListsLabelId < ActiveRecord::Migration class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddDeletedAtToNamespaces < ActiveRecord::Migration class AddDeletedAtToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForBuildToken < ActiveRecord::Migration class AddIndexForBuildToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveRedundantIndexes < ActiveRecord::Migration class RemoveRedundantIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToNoteDiscussionId < ActiveRecord::Migration class AddIndexToNoteDiscussionId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIncomingEmailTokenToUsers < ActiveRecord::Migration class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddGroupIdToLabels < ActiveRecord::Migration class AddGroupIdToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsTitle < ActiveRecord::Migration class AddIndexToLabelsTitle < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToLabels < ActiveRecord::Migration class AddUniqueIndexToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToSubscriptions < ActiveRecord::Migration class AddUniqueIndexToSubscriptions < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddProjectImportDataProjectIndex < ActiveRecord::Migration class AddProjectImportDataProjectIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToParentId < ActiveRecord::Migration class AddIndexToParentId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUnnecessaryIndexes < ActiveRecord::Migration class RemoveUnnecessaryIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToRoutes < ActiveRecord::Migration class AddIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddPathIndexToNamespace < ActiveRecord::Migration class AddPathIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddNameIndexToNamespace < ActiveRecord::Migration class AddNameIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddLowerPathIndexToRoutes < ActiveRecord::Migration class AddLowerPathIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToProjectAuthorizations < ActiveRecord::Migration class AddIndexToProjectAuthorizations < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
...@@ -6,8 +7,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration ...@@ -6,8 +7,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
def up def up
add_concurrent_index(:project_authorizations, :project_id) unless unless index_exists?(:project_authorizations, :project_id)
index_exists?(:project_authorizations, :project_id) add_concurrent_index(:project_authorizations, :project_id)
end
end end
def down def down
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddRelativePositionToIssues < ActiveRecord::Migration class AddRelativePositionToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToUserAgentDetail < ActiveRecord::Migration class AddIndexToUserAgentDetail < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
......
# rubocop:disable RemoveIndex
class DropIndexForBuildsProjectStatus < ActiveRecord::Migration class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
......
# rubocop:disable RemoveIndex
class RemoveOldProjectIdColumns < ActiveRecord::Migration class RemoveOldProjectIdColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToUserGhost < ActiveRecord::Migration class AddIndexToUserGhost < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
class AddImportJidToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :projects, :import_jid, :string
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170402231018) do ActiveRecord::Schema.define(version: 20170405080720) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -922,6 +922,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do ...@@ -922,6 +922,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do
t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.integer "auto_cancel_pending_pipelines", default: 0, null: false t.integer "auto_cancel_pending_pipelines", default: 0, null: false
t.boolean "printing_merge_request_link_enabled", default: true, null: false t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -58,10 +58,22 @@ migration was tested. ...@@ -58,10 +58,22 @@ migration was tested.
## Removing indices ## Removing indices
If you need to remove index, please add a condition like in following example: When removing an index make sure to use the method `remove_concurrent_index` instead
of the regular `remove_index` method. The `remove_concurrent_index` method
automatically drops concurrent indexes when using PostgreSQL, removing the
need for downtime. To use this method you must disable transactions by calling
the method `disable_ddl_transaction!` in the body of your migration class like
so:
```ruby ```ruby
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
remove_concurrent_index :table_name, :column_name if index_exists?(:table_name, :column_name)
end
end
``` ```
## Adding indices ## Adding indices
......
...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration ...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime. # migration requires downtime.
# DOWNTIME_REASON = '' # DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default" # When using the methods "add_concurrent_index", "remove_concurrent_index" or
# you must disable the use of transactions as these methods can not run in an # "add_column_with_default" you must disable the use of transactions
# existing transaction. When using "add_concurrent_index" make sure that this # as these methods can not run in an existing transaction.
# method is the _only_ method called in the migration, any other changes # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# should go in a separate migration. This ensures that upon failure _only_ the # that either of them is the _only_ method called in the migration,
# index creation fails and can be retried or reverted easily. # any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
# #
# To disable transactions uncomment the following line and remove these # To disable transactions uncomment the following line and remove these
# comments: # comments:
......
...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration ...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime. # migration requires downtime.
# DOWNTIME_REASON = '' # DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default" # When using the methods "add_concurrent_index", "remove_concurrent_index" or
# you must disable the use of transactions as these methods can not run in an # "add_column_with_default" you must disable the use of transactions
# existing transaction. When using "add_concurrent_index" make sure that this # as these methods can not run in an existing transaction.
# method is the _only_ method called in the migration, any other changes # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# should go in a separate migration. This ensures that upon failure _only_ the # that either of them is the _only_ method called in the migration,
# index creation fails and can be retried or reverted easily. # any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
# #
# To disable transactions uncomment the following line and remove these # To disable transactions uncomment the following line and remove these
# comments: # comments:
......
...@@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration ...@@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default" # When using the methods "add_concurrent_index", "remove_concurrent_index" or
# you must disable the use of transactions as these methods can not run in an # "add_column_with_default" you must disable the use of transactions
# existing transaction. When using "add_concurrent_index" make sure that this # as these methods can not run in an existing transaction.
# method is the _only_ method called in the migration, any other changes # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# should go in a separate migration. This ensures that upon failure _only_ the # that either of them is the _only_ method called in the migration,
# index creation fails and can be retried or reverted easily. # any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
# #
# To disable transactions uncomment the following line and remove these # To disable transactions uncomment the following line and remove these
# comments: # comments:
......
...@@ -47,6 +47,7 @@ module API ...@@ -47,6 +47,7 @@ module API
params do params do
requires :key, type: String, desc: 'The new deploy key' requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key' requires :title, type: String, desc: 'The name of the deploy key'
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
end end
post ":id/deploy_keys" do post ":id/deploy_keys" do
params[:key].strip! params[:key].strip!
......
...@@ -142,7 +142,7 @@ module API ...@@ -142,7 +142,7 @@ module API
end end
get ":id/projects" do get ":id/projects" do
group = find_group!(params[:id]) group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user) projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects) projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user present paginate(projects), with: entity, current_user: current_user
......
...@@ -84,7 +84,7 @@ module API ...@@ -84,7 +84,7 @@ module API
end end
get do get do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics] present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity, statistics: params[:statistics]
end end
desc 'Create new project' do desc 'Create new project' do
......
...@@ -341,7 +341,7 @@ module API ...@@ -341,7 +341,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
events = user.events. events = user.events.
merge(ProjectsFinder.new.execute(current_user)). merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project). references(:project).
with_associations. with_associations.
recent recent
......
...@@ -151,7 +151,7 @@ module API ...@@ -151,7 +151,7 @@ module API
end end
get ":id/projects" do get ":id/projects" do
group = find_group!(params[:id]) group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user) projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects) projects = filter_projects(projects)
entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user present paginate(projects), with: entity, current_user: current_user
......
...@@ -107,7 +107,7 @@ module API ...@@ -107,7 +107,7 @@ module API
end end
get '/visible' do get '/visible' do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
end end
desc 'Get a projects list for authenticated user' do desc 'Get a projects list for authenticated user' do
......
...@@ -138,7 +138,7 @@ module API ...@@ -138,7 +138,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
events = user.events. events = user.events.
merge(ProjectsFinder.new.execute(current_user)). merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project). references(:project).
with_associations. with_associations.
recent recent
......
...@@ -26,6 +26,30 @@ module Gitlab ...@@ -26,6 +26,30 @@ module Gitlab
add_index(table_name, column_name, options) add_index(table_name, column_name, options)
end end
# Removes an existed index, concurrently when supported
#
# On PostgreSQL this method removes an index concurrently.
#
# Example:
#
# remove_concurrent_index :users, :some_column
#
# See Rails' `remove_index` for more info on the available arguments.
def remove_concurrent_index(table_name, column_name, options = {})
if transaction_open?
raise 'remove_concurrent_index can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
'in the body of your migration class'
end
if Database.postgresql?
options = options.merge({ algorithm: :concurrently })
disable_statement_timeout
end
remove_index(table_name, options.merge({ column: column_name }))
end
# Adds a foreign key with only minimal locking on the tables involved. # Adds a foreign key with only minimal locking on the tables involved.
# #
# This method only requires minimal locking when using PostgreSQL. When # This method only requires minimal locking when using PostgreSQL. When
......
...@@ -72,6 +72,8 @@ module Gitlab ...@@ -72,6 +72,8 @@ module Gitlab
# job_ids - The Sidekiq job IDs to check. # job_ids - The Sidekiq job IDs to check.
# #
# Returns an array of true or false indicating job completion. # Returns an array of true or false indicating job completion.
# true = job is still running
# false = job completed
def self.job_status(job_ids) def self.job_status(job_ids)
keys = job_ids.map { |jid| key_for(jid) } keys = job_ids.map { |jid| key_for(jid) }
...@@ -82,6 +84,17 @@ module Gitlab ...@@ -82,6 +84,17 @@ module Gitlab
end end
end end
# Returns the JIDs that are completed
#
# job_ids - The Sidekiq job IDs to check.
#
# Returns an array of completed JIDs
def self.completed_jids(job_ids)
Sidekiq.redis do |redis|
job_ids.reject { |jid| redis.exists(key_for(jid)) }
end
end
def self.key_for(jid) def self.key_for(jid)
STATUS_KEY % jid STATUS_KEY % jid
end end
......
...@@ -2,7 +2,9 @@ module Gitlab ...@@ -2,7 +2,9 @@ module Gitlab
module SidekiqStatus module SidekiqStatus
class ClientMiddleware class ClientMiddleware
def call(_, job, _, _) def call(_, job, _, _)
Gitlab::SidekiqStatus.set(job['jid']) status_expiration = job['status_expiration'] || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION
Gitlab::SidekiqStatus.set(job['jid'], status_expiration)
yield yield
end end
end end
......
...@@ -9,7 +9,7 @@ module RuboCop ...@@ -9,7 +9,7 @@ module RuboCop
include MigrationHelpers include MigrationHelpers
MSG = '`add_concurrent_index` is not reversible so you must manually define ' \ MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze 'the `up` and `down` methods in your migration class, using `remove_concurrent_index` in `down`'.freeze
def on_send(node) def on_send(node)
return unless in_migration?(node) return unless in_migration?(node)
......
require_relative '../../migration_helpers'
module RuboCop
module Cop
module Migration
# Cop that checks if `remove_concurrent_index` is used with `up`/`down` methods
# and not `change`.
class RemoveConcurrentIndex < RuboCop::Cop::Cop
include MigrationHelpers
MSG = '`remove_concurrent_index` is not reversible so you must manually define ' \
'the `up` and `down` methods in your migration class, using `add_concurrent_index` in `down`'.freeze
def on_send(node)
return unless in_migration?(node)
return unless node.children[1] == :remove_concurrent_index
node.each_ancestor(:def) do |def_node|
add_offense(def_node, :name) if method_name(def_node) == :change
end
end
def method_name(node)
node.children[0]
end
end
end
end
end
require_relative '../../migration_helpers'
module RuboCop
module Cop
module Migration
# Cop that checks if indexes are removed in a concurrent manner.
class RemoveIndex < RuboCop::Cop::Cop
include MigrationHelpers
MSG = '`remove_index` requires downtime, use `remove_concurrent_index` instead'.freeze
def on_def(node)
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
add_offense(send_node, :selector) if method_name(send_node) == :remove_index
end
end
def method_name(node)
node.children[1]
end
end
end
end
end
...@@ -5,3 +5,5 @@ require_relative 'cop/migration/add_column_with_default' ...@@ -5,3 +5,5 @@ require_relative 'cop/migration/add_column_with_default'
require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index' require_relative 'cop/migration/add_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
...@@ -23,5 +23,9 @@ FactoryGirl.define do ...@@ -23,5 +23,9 @@ FactoryGirl.define do
factory :another_deploy_key, class: 'DeployKey' do factory :another_deploy_key, class: 'DeployKey' do
end end
end end
factory :write_access_key, class: 'DeployKey' do
can_push true
end
end end
end end
...@@ -3,8 +3,9 @@ require 'spec_helper' ...@@ -3,8 +3,9 @@ require 'spec_helper'
describe GroupProjectsFinder do describe GroupProjectsFinder do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
let(:options) { {} }
let(:finder) { described_class.new(source_user) } let(:finder) { described_class.new(group: group, current_user: current_user, options: options) }
let!(:public_project) { create(:empty_project, :public, group: group, path: '1') } let!(:public_project) { create(:empty_project, :public, group: group, path: '1') }
let!(:private_project) { create(:empty_project, :private, group: group, path: '2') } let!(:private_project) { create(:empty_project, :private, group: group, path: '2') }
...@@ -18,22 +19,27 @@ describe GroupProjectsFinder do ...@@ -18,22 +19,27 @@ describe GroupProjectsFinder do
shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group) shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
end end
subject { finder.execute }
describe 'with a group member current user' do describe 'with a group member current user' do
before { group.add_user(current_user, Gitlab::Access::MASTER) } before do
group.add_master(current_user)
end
context "only shared" do context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) } let(:options) { { only_shared: true } }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end end
context "only owned" do context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) } let(:options) { { only_owned: true } }
it { is_expected.to eq([private_project, public_project]) }
it { is_expected.to match_array([private_project, public_project]) }
end end
context "all" do context "all" do
subject { described_class.new(group).execute(current_user) } it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
end end
end end
...@@ -44,47 +50,57 @@ describe GroupProjectsFinder do ...@@ -44,47 +50,57 @@ describe GroupProjectsFinder do
end end
context "only shared" do context "only shared" do
let(:options) { { only_shared: true } }
context "without external user" do context "without external user" do
subject { described_class.new(group, only_shared: true).execute(current_user) } it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
end end
context "with external user" do context "with external user" do
before { current_user.update_attributes(external: true) } before do
subject { described_class.new(group, only_shared: true).execute(current_user) } current_user.update_attributes(external: true)
it { is_expected.to eq([shared_project_2, shared_project_1]) } end
it { is_expected.to match_array([shared_project_2, shared_project_1]) }
end end
end end
context "only owned" do context "only owned" do
let(:options) { { only_owned: true } }
context "without external user" do context "without external user" do
before { private_project.team << [current_user, Gitlab::Access::MASTER] } before do
subject { described_class.new(group, only_owned: true).execute(current_user) } private_project.team << [current_user, Gitlab::Access::MASTER]
it { is_expected.to eq([private_project, public_project]) } end
it { is_expected.to match_array([private_project, public_project]) }
end end
context "with external user" do context "with external user" do
before { current_user.update_attributes(external: true) } before do
subject { described_class.new(group, only_owned: true).execute(current_user) } current_user.update_attributes(external: true)
it { is_expected.to eq([public_project]) } end
end
context "all" do it { is_expected.to eq([public_project]) }
subject { described_class.new(group).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) }
end end
end end
context "all" do
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
end
end end
describe "no user" do describe "no user" do
context "only shared" do context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) } let(:options) { { only_shared: true } }
it { is_expected.to eq([shared_project_3, shared_project_1]) }
it { is_expected.to match_array([shared_project_3, shared_project_1]) }
end end
context "only owned" do context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) } let(:options) { { only_owned: true } }
it { is_expected.to eq([public_project]) }
it { is_expected.to eq([public_project]) }
end end
end end
end end
...@@ -21,38 +21,144 @@ describe ProjectsFinder do ...@@ -21,38 +21,144 @@ describe ProjectsFinder do
create(:empty_project, :private, name: 'D', path: 'D') create(:empty_project, :private, name: 'D', path: 'D')
end end
let(:finder) { described_class.new } let(:params) { {} }
let(:current_user) { user }
let(:project_ids_relation) { nil }
let(:finder) { described_class.new(params: params, current_user: current_user, project_ids_relation: project_ids_relation) }
subject { finder.execute }
describe 'without a user' do describe 'without a user' do
subject { finder.execute } let(:current_user) { nil }
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
describe 'with a user' do describe 'with a user' do
subject { finder.execute(user) }
describe 'without private projects' do describe 'without private projects' do
it { is_expected.to eq([public_project, internal_project]) } it { is_expected.to match_array([public_project, internal_project]) }
end end
describe 'with private projects' do describe 'with private projects' do
before do before do
private_project.add_user(user, Gitlab::Access::MASTER) private_project.add_master(user)
end end
it do it { is_expected.to match_array([public_project, internal_project, private_project]) }
is_expected.to eq([public_project, internal_project, private_project])
end
end end
end end
describe 'with project_ids_relation' do describe 'with project_ids_relation' do
let(:project_ids_relation) { Project.where(id: internal_project.id) } let(:project_ids_relation) { Project.where(id: internal_project.id) }
subject { finder.execute(user, project_ids_relation) }
it { is_expected.to eq([internal_project]) } it { is_expected.to eq([internal_project]) }
end end
describe 'filter by visibility_level' do
before do
private_project.add_master(user)
end
context 'private' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
it { is_expected.to eq([private_project]) }
end
context 'internal' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::INTERNAL } }
it { is_expected.to eq([internal_project]) }
end
context 'public' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
it { is_expected.to eq([public_project]) }
end
end
describe 'filter by tags' do
before do
public_project.tag_list.add('foo')
public_project.save!
end
let(:params) { { tag: 'foo' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by personal' do
let!(:personal_project) { create(:empty_project, namespace: user.namespace) }
let(:params) { { personal: true } }
it { is_expected.to eq([personal_project]) }
end
describe 'filter by search' do
let(:params) { { search: 'C' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by name for backward compatibility' do
let(:params) { { name: 'C' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by archived' do
let!(:archived_project) { create(:empty_project, :public, :archived, name: 'E', path: 'E') }
context 'non_archived=true' do
let(:params) { { non_archived: true } }
it { is_expected.to match_array([public_project, internal_project]) }
end
context 'non_archived=false' do
let(:params) { { non_archived: false } }
it { is_expected.to match_array([public_project, internal_project, archived_project]) }
end
describe 'filter by archived for backward compatibility' do
let(:params) { { archived: false } }
it { is_expected.to match_array([public_project, internal_project]) }
end
end
describe 'filter by trending' do
let!(:trending_project) { create(:trending_project, project: public_project) }
let(:params) { { trending: true } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by non_public' do
let(:params) { { non_public: true } }
before do
private_project.add_developer(current_user)
end
it { is_expected.to eq([private_project]) }
end
describe 'filter by viewable_starred_projects' do
let(:params) { { starred: true } }
before do
current_user.toggle_star(public_project)
end
it { is_expected.to eq([public_project]) }
end
describe 'sorting' do
let(:params) { { sort: 'name_asc' } }
it { is_expected.to eq([internal_project, public_project]) }
end
end end
end end
import {
BoxGeometry,
} from 'three/build/three.module';
import MeshObject from '~/blob/3d_viewer/mesh_object';
describe('Mesh object', () => {
it('defaults to non-wireframe material', () => {
const object = new MeshObject(
new BoxGeometry(10, 10, 10),
);
expect(object.material.wireframe).toBeFalsy();
});
it('changes to wirefame material', () => {
const object = new MeshObject(
new BoxGeometry(10, 10, 10),
);
object.changeMaterial('wireframe');
expect(object.material.wireframe).toBeTruthy();
});
it('scales object down', () => {
const object = new MeshObject(
new BoxGeometry(10, 10, 10),
);
const radius = object.geometry.boundingSphere.radius;
expect(radius).not.toBeGreaterThan(4);
});
it('does not scale object down', () => {
const object = new MeshObject(
new BoxGeometry(1, 1, 1),
);
const radius = object.geometry.boundingSphere.radius;
expect(radius).toBeLessThan(1);
});
});
...@@ -3,6 +3,18 @@ import testPDF from './test.pdf'; ...@@ -3,6 +3,18 @@ import testPDF from './test.pdf';
describe('PDF renderer', () => { describe('PDF renderer', () => {
let viewer; let viewer;
let app;
const checkLoaded = (done) => {
if (app.loading) {
setTimeout(() => {
checkLoaded(done);
}, 100);
} else {
done();
}
};
preloadFixtures('static/pdf_viewer.html.raw'); preloadFixtures('static/pdf_viewer.html.raw');
beforeEach(() => { beforeEach(() => {
...@@ -21,11 +33,9 @@ describe('PDF renderer', () => { ...@@ -21,11 +33,9 @@ describe('PDF renderer', () => {
describe('successful response', () => { describe('successful response', () => {
beforeEach((done) => { beforeEach((done) => {
renderPDF(); app = renderPDF();
setTimeout(() => { checkLoaded(done);
done();
}, 500);
}); });
it('does not show loading icon', () => { it('does not show loading icon', () => {
...@@ -50,11 +60,9 @@ describe('PDF renderer', () => { ...@@ -50,11 +60,9 @@ describe('PDF renderer', () => {
describe('error getting file', () => { describe('error getting file', () => {
beforeEach((done) => { beforeEach((done) => {
viewer.dataset.endpoint = 'invalid/endpoint'; viewer.dataset.endpoint = 'invalid/endpoint';
renderPDF(); app = renderPDF();
setTimeout(() => { checkLoaded(done);
done();
}, 500);
}); });
it('does not show loading icon', () => { it('does not show loading icon', () => {
......
...@@ -75,6 +75,7 @@ describe('Build', () => { ...@@ -75,6 +75,7 @@ describe('Build', () => {
expect(url).toBe(`${BUILD_URL}.json`); expect(url).toBe(`${BUILD_URL}.json`);
expect(dataType).toBe('json'); expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function)); expect(success).toEqual(jasmine.any(Function));
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'running' }); success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
...@@ -83,6 +84,7 @@ describe('Build', () => { ...@@ -83,6 +84,7 @@ describe('Build', () => {
it('removes the spinner', () => { it('removes the spinner', () => {
const [{ success, context }] = $.ajax.calls.argsFor(0); const [{ success, context }] = $.ajax.calls.argsFor(0);
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0); expect($('.js-build-refresh').length).toBe(0);
......
...@@ -310,5 +310,56 @@ require('~/lib/utils/common_utils'); ...@@ -310,5 +310,56 @@ require('~/lib/utils/common_utils');
}); });
}, 10000); }, 10000);
}); });
describe('gl.utils.setFavicon', () => {
it('should set page favicon to provided favicon', () => {
const faviconName = 'custom_favicon';
const fakeLink = {
setAttribute() {},
};
spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
expect(attr).toEqual('href');
expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true);
});
gl.utils.setFavicon(faviconName);
});
});
describe('gl.utils.resetFavicon', () => {
it('should reset page favicon to tanuki', () => {
const fakeLink = {
setAttribute() {},
};
spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
expect(attr).toEqual('href');
expect(val).toMatch(/favicon/);
});
gl.utils.resetFavicon();
});
});
describe('gl.utils.setCiStatusFavicon', () => {
it('should set page favicon to CI status favicon based on provided status', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`;
const FAVICON_PATH = 'ci_favicons/';
const FAVICON = 'icon_status_success';
const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub();
const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub();
spyOn($, 'ajax').and.callFake(function (options) {
options.success({ icon: FAVICON });
expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON);
options.success();
expect(spyResetFavicon).toHaveBeenCalled();
options.error();
expect(spyResetFavicon).toHaveBeenCalled();
});
gl.utils.setCiStatusFavicon(BUILD_URL);
});
});
}); });
})(); })();
...@@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility'); ...@@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility');
it('should call showCIStatus even if a notification should not be displayed', function() { it('should call showCIStatus even if a notification should not be displayed', function() {
var spy; var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub(); spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
}); });
it('should call showCIStatus when a notification should be displayed', function() { it('should call showCIStatus when a notification should be displayed', function() {
var spy; var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub(); spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(true); this["class"].getCIStatus(true);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
}); });
it('should call showCICoverage when the coverage rate is set', function() { it('should call showCICoverage when the coverage rate is set', function() {
var spy; var spy;
spy = spyOn(this["class"], 'showCICoverage').and.stub(); spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
}); });
...@@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility'); ...@@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility');
var spy; var spy;
this.ciStatusData.coverage = null; this.ciStatusData.coverage = null;
spy = spyOn(this["class"], 'showCICoverage').and.stub(); spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled(); return expect(spy).not.toHaveBeenCalled();
}); });
it('should not display a notification on the first check after the widget has been created', function() { it('should not display a notification on the first check after the widget has been created', function() {
var spy; var spy;
spy = spyOn(window, 'notify'); spy = spyOn(window, 'notify');
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"] = new window.gl.MergeRequestWidget(this.opts); this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true); this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled(); return expect(spy).not.toHaveBeenCalled();
...@@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility'); ...@@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility');
it('should update the pipeline URL when the pipeline changes', function() { it('should update the pipeline URL when the pipeline changes', function() {
var spy; var spy;
spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
this.ciStatusData.pipeline += 1; this.ciStatusData.pipeline += 1;
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
...@@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility'); ...@@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility');
it('should update the commit URL when the sha changes', function() { it('should update the commit URL when the sha changes', function() {
var spy; var spy;
spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
this.ciStatusData.sha = "9b50b99a"; this.ciStatusData.sha = "9b50b99a";
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
......
...@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end end
end end
describe '#remove_concurrent_index' do
context 'outside a transaction' do
before do
allow(model).to receive(:transaction_open?).and_return(false)
end
context 'using PostgreSQL' do
before do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
allow(model).to receive(:disable_statement_timeout)
end
it 'removes the index concurrently' do
expect(model).to receive(:remove_index).
with(:users, { algorithm: :concurrently, column: :foo })
model.remove_concurrent_index(:users, :foo)
end
end
context 'using MySQL' do
it 'removes an index' do
expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(model).to receive(:remove_index).
with(:users, { column: :foo })
model.remove_concurrent_index(:users, :foo)
end
end
end
context 'inside a transaction' do
it 'raises RuntimeError' do
expect(model).to receive(:transaction_open?).and_return(true)
expect { model.remove_concurrent_index(:users, :foo) }.
to raise_error(RuntimeError)
end
end
end
describe '#add_concurrent_foreign_key' do describe '#add_concurrent_foreign_key' do
context 'inside a transaction' do context 'inside a transaction' do
it 'raises an error' do it 'raises an error' do
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::SidekiqStatus::ClientMiddleware do describe Gitlab::SidekiqStatus::ClientMiddleware do
describe '#call' do describe '#call' do
it 'tracks the job in Redis' do it 'tracks the job in Redis' do
expect(Gitlab::SidekiqStatus).to receive(:set).with('123') expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
described_class.new. described_class.new.
call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil }
......
...@@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do ...@@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do
expect(key).to include('123') expect(key).to include('123')
end end
end end
describe 'completed', :redis do
it 'returns the completed job' do
expect(described_class.completed_jids(%w(123))).to eq(['123'])
end
it 'returns only the jobs completed' do
described_class.set('123')
described_class.set('456')
expect(described_class.completed_jids(%w(123 456 789))).to eq(['789'])
end
end
end end
class NotifyPreview < ActionMailer::Preview
def pipeline_success_email
pipeline = Ci::Pipeline.last
Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
end
def pipeline_failed_email
pipeline = Ci::Pipeline.last
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
end
...@@ -111,6 +111,20 @@ describe Blob do ...@@ -111,6 +111,20 @@ describe Blob do
end end
end end
describe '#stl?' do
it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'file.png')
expect(described_class.decorate(git_blob)).not_to be_stl
end
it 'is truthy with STL extension' do
git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
expect(described_class.decorate(git_blob)).to be_stl
end
end
describe '#to_partial_path' do describe '#to_partial_path' do
let(:project) { double(lfs_enabled?: true) } let(:project) { double(lfs_enabled?: true) }
...@@ -122,7 +136,8 @@ describe Blob do ...@@ -122,7 +136,8 @@ describe Blob do
lfs_pointer?: false, lfs_pointer?: false,
svg?: false, svg?: false,
text?: false, text?: false,
binary?: false binary?: false,
stl?: false
) )
described_class.decorate(double).tap do |blob| described_class.decorate(double).tap do |blob|
...@@ -175,6 +190,11 @@ describe Blob do ...@@ -175,6 +190,11 @@ describe Blob do
blob = stubbed_blob(text?: true, sketch?: true, binary?: true) blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
expect(blob.to_partial_path(project)).to eq 'sketch' expect(blob.to_partial_path(project)).to eq 'sketch'
end end
it 'handles STLs' do
blob = stubbed_blob(text?: true, stl?: true)
expect(blob.to_partial_path(project)).to eq 'stl'
end
end end
describe '#size_within_svg_limits?' do describe '#size_within_svg_limits?' do
......
...@@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do ...@@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
end end
it 'accepts can_push parameter' do
key_attrs = attributes_for :write_access_key
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
expect(response).to have_http_status(201)
expect(json_response['can_push']).to eq(true)
end
end end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do describe 'DELETE /projects/:id/deploy_keys/:key_id' do
......
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/migration/remove_concurrent_index'
describe RuboCop::Cop::Migration::RemoveConcurrentIndex do
include CopHelper
subject(:cop) { described_class.new }
context 'in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
end
it 'registers an offense when remove_concurrent_index is used inside a change method' do
inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
end
end
it 'registers no offense when remove_concurrent_index is used inside an up method' do
inspect_source(cop, 'def up; remove_concurrent_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
end
context 'outside of migration' do
it 'registers no offense' do
inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
end
end
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/migration/remove_index'
describe RuboCop::Cop::Migration::RemoveIndex do
include CopHelper
subject(:cop) { described_class.new }
context 'in migration' do
before do
allow(cop).to receive(:in_migration?).and_return(true)
end
it 'registers an offense when remove_index is used' do
inspect_source(cop, 'def change; remove_index :table, :column; end')
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([1])
end
end
end
context 'outside of migration' do
it 'registers no offense' do
inspect_source(cop, 'def change; remove_index :table, :column; end')
expect(cop.offenses.size).to eq(0)
end
end
end
...@@ -49,6 +49,7 @@ describe MergeRequests::RefreshService, services: true do ...@@ -49,6 +49,7 @@ describe MergeRequests::RefreshService, services: true do
context 'push to origin repo source branch' do context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) } let(:refresh_service) { service.new(@project, @user) }
before do before do
allow(refresh_service).to receive(:execute_hooks) allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
...@@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do ...@@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do
end end
end end
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
before do
@merge_request.update(state: :reopened)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
with(@merge_request, 'update', @oldrev)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
expect(@build_failed_todo).to be_done
expect(@fork_build_failed_todo).to be_done
end
end
context 'push to origin repo target branch' do context 'push to origin repo target branch' do
before do before do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
......
require 'spec_helper'
describe 'notify/pipeline_failed_email.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
user: user,
ref: project.default_branch,
sha: project.commit.sha,
status: :success)
end
before do
assign(:project, project)
assign(:pipeline, pipeline)
assign(:merge_request, merge_request)
end
context 'pipeline with user' do
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has failed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content pipeline.user.name
end
end
context 'pipeline without user' do
before do
pipeline.update_attribute(:user, nil)
end
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has failed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content "by API"
end
end
end
require 'spec_helper'
describe 'notify/pipeline_success_email.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
user: user,
ref: project.default_branch,
sha: project.commit.sha,
status: :success)
end
before do
assign(:project, project)
assign(:pipeline, pipeline)
assign(:merge_request, merge_request)
end
context 'pipeline with user' do
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has passed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content pipeline.user.name
end
end
context 'pipeline without user' do
before do
pipeline.update_attribute(:user, nil)
end
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has passed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content "by API"
end
end
end
...@@ -23,10 +23,12 @@ describe RepositoryImportWorker do ...@@ -23,10 +23,12 @@ describe RepositoryImportWorker do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
expect_any_instance_of(Projects::ImportService).to receive(:execute). expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error }) and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123')
subject.perform(project.id) subject.perform(project.id)
expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/") expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
expect(project.reload.import_jid).not_to be_nil
end end
end end
end end
......
require 'spec_helper'
describe StuckImportJobsWorker do
let(:worker) { described_class.new }
let(:exclusive_lease_uuid) { SecureRandom.uuid }
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end
describe 'long running import' do
let(:project) { create(:empty_project, import_jid: '123', import_status: 'started') }
before do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123'])
end
it 'marks the project as failed' do
expect { worker.perform }.to change { project.reload.import_status }.to('failed')
end
end
describe 'running import' do
let(:project) { create(:empty_project, import_jid: '123', import_status: 'started') }
before do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
end
it 'does not mark the project as failed' do
worker.perform
expect(project.reload.import_status).to eq('started')
end
end
end
...@@ -4305,6 +4305,18 @@ text-table@~0.2.0: ...@@ -4305,6 +4305,18 @@ text-table@~0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
three-orbit-controls@^82.1.0:
version "82.1.0"
resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
three-stl-loader@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/three-stl-loader/-/three-stl-loader-1.0.4.tgz#6b3319a31e3b910aab1883d19b00c81a663c3e03"
three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
throttleit@^1.0.0: throttleit@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
......
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