Commit 04b03597 authored by Pawel Chojnacki's avatar Pawel Chojnacki

Merge remote-tracking branch 'upstream/master' into 5029-support-cluster-metrics

parents 042b4720 e1e1c338
......@@ -686,9 +686,10 @@ codequality:
cache: {}
dependencies: []
script:
- apk update && apk add jq
- ./scripts/codequality analyze -f json > raw_codeclimate.json || true
# The following line keeps only the fields used in the MR widget, reducing the JSON artifact size
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,description,fingerprint,location})' > codeclimate.json
- jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json
artifacts:
paths: [codeclimate.json]
expire_in: 1 week
......
......@@ -197,6 +197,17 @@ release. There are two levels of priority labels:
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
### Severity labels (~S1, ~S2, etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Example |
|-------|------------------------------------------|---------|
| ~S1 | Feature broken, no workaround | Unable to create an issue |
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
### Label for community contributors (~"Accepting Merge Requests")
Issues that are beneficial to our users, 'nice to haves', that we currently do
......
......@@ -426,7 +426,11 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.87.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
# Explicitly lock grpc as we know 1.9 is bad
# 1.10 is still being tested. See gitlab-org/gitaly#1059
gem 'grpc', '~> 1.8.3'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
......
......@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.87.0)
gitaly-proto (0.88.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (5.3.3)
......@@ -630,7 +630,7 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.3.0)
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
......@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.87.0)
gitaly-proto (~> 0.88.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1108,6 +1108,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.8.3)
gssapi
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
......
......@@ -12,7 +12,7 @@ $(() => {
const $container = $(container);
$container
.find('.js-toggle-button .fa')
.find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
......@@ -22,7 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.target.classList.toggle('open');
e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
......
......@@ -7,6 +7,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super({
page: 'boards',
isGroup: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysIssues,
stateFiltersSelector: '.issues-state-filters',
});
......
......@@ -20,10 +20,6 @@
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
......@@ -45,23 +41,14 @@
},
computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
......@@ -92,25 +79,22 @@
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
:label="s__('Pipelines|Loading Pipelines')"
size="3"
v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath"
/>
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
<svg-blank-state
v-else-if="shouldRenderErrorState"
:svg-path="errorStateSvgPath"
:message="s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)"
/>
<div
class="table-holder"
v-if="shouldRenderTable"
v-else-if="shouldRenderTable"
>
<pipelines-table-component
:pipelines="state.pipelines"
......
......@@ -125,6 +125,16 @@ export default class FilteredSearchDropdownManager {
endpoint = `${endpoint}?only_group_labels=true`;
}
// EE-only
if (this.groupAncestor) {
endpoint = `${endpoint}&include_ancestor_groups=true`;
}
// EE-only
if (this.isGroupDecendent) {
endpoint = `${endpoint}&include_descendant_groups=true`;
}
return endpoint;
}
......
......@@ -109,6 +109,7 @@ export default class FilteredSearchManager {
page: this.page,
isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor,
isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
......
......@@ -152,14 +152,14 @@ export default {
showLeaveGroupModal(group, parentGroup) {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.updateModal = true;
this.showModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
},
hideLeaveGroupModal() {
this.updateModal = false;
this.showModal = false;
},
leaveGroup() {
this.updateModal = false;
this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
......@@ -208,9 +208,9 @@ export default {
:page-info="pageInfo"
/>
<modal
v-show="showModal"
:primary-button-label="__('Leave')"
v-if="showModal"
kind="warning"
:primary-button-label="__('Leave')"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
@cancel="hideLeaveGroupModal"
......
......@@ -8,4 +8,5 @@ export default {
OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
NOT_FOUND: 404,
};
......@@ -216,6 +216,9 @@ export default class MilestoneSelect {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
})
.catch(() => {
$loading.fadeOut();
});
}
}
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate);
......@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
pipelinesComponent,
},
data() {
const store = new PipelinesStore();
return {
store,
store: new PipelinesStore(),
};
},
created() {
this.dataset = document.querySelector(this.$options.el).dataset;
},
render(createElement) {
return createElement('pipelines-component', {
props: {
store: this.store,
endpoint: this.dataset.endpoint,
helpPagePath: this.dataset.helpPagePath,
emptyStateSvgPath: this.dataset.emptyStateSvgPath,
errorStateSvgPath: this.dataset.errorStateSvgPath,
noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
autoDevopsPath: this.dataset.helpAutoDevopsPath,
newPipelinePath: this.dataset.newPipelinePath,
canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath,
},
});
},
......
<script>
export default {
props: {
errorStateSvgPath: {
type: String,
required: true,
export default {
name: 'PipelinesSvgState',
props: {
svgPath: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
},
},
};
};
</script>
<template>
<div class="row empty-state js-pipelines-error-state">
<div class="row empty-state">
<div class="col-xs-12">
<div class="svg-content">
<img :src="errorStateSvgPath"/>
<img :src="svgPath" />
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
<h4>{{ message }}</h4>
</div>
</div>
</div>
......
<script>
export default {
name: 'PipelinesEmptyState',
props: {
helpPagePath: {
type: String,
......@@ -9,6 +10,10 @@
type: String,
required: true,
},
canSetCi: {
type: Boolean,
required: true,
},
},
};
</script>
......@@ -22,22 +27,36 @@
<div class="col-xs-12">
<div class="text-content">
<h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }}
</h4>
<p>
{{ s__(`Pipelines|Continous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.`) }}
<template v-if="canSetCi">
<h4 class="text-center">
{{ s__('Pipelines|Build with confidence') }}
</h4>
<p>
{{ s__(`Pipelines|Continous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`) }}
</p>
<div class="text-center">
<a
:href="helpPagePath"
class="btn btn-primary js-get-started-pipelines"
>
{{ s__('Pipelines|Get started with Pipelines') }}
</a>
</div>
</template>
<p
v-else
class="text-center"
>
{{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
<div class="text-center">
<a
:href="helpPagePath"
class="btn btn-info"
>
{{ s__("Pipelines|Get started with Pipelines") }}
</a>
</div>
</div>
</div>
</div>
......
<script>
export default {
name: 'PipelineNavControls',
props: {
newPipelinePath: {
type: String,
required: true,
export default {
name: 'PipelineNavControls',
props: {
newPipelinePath: {
type: String,
required: false,
default: null,
},
resetCachePath: {
type: String,
required: false,
default: null,
},
ciLintPath: {
type: String,
required: false,
default: null,
},
},
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
resetCachePath: {
type: String,
required: true,
},
ciLintPath: {
type: String,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
},
},
};
};
</script>
<template>
<div class="nav-controls">
<a
v-if="canCreatePipeline"
v-if="newPipelinePath"
:href="newPipelinePath"
class="btn btn-create">
Run Pipeline
</a>
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
class="btn btn-create js-run-pipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
</a>
<a
v-if="resetCachePath"
data-method="post"
rel="nofollow"
:href="resetCachePath"
class="btn btn-default">
Clear runner caches
class="btn btn-default js-clear-cache"
>
{{ s__('Pipelines|Clear Runner Caches') }}
</a>
<a
v-if="ciLintPath"
:href="ciLintPath"
class="btn btn-default">
CI Lint
class="btn btn-default js-ci-lint"
>
{{ s__('Pipelines|CI Lint') }}
</a>
</div>
</template>
import Visibility from 'visibilityjs';
import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import pipelinesTableComponent from '../components/pipelines_table.vue';
import EmptyState from '../components/empty_state.vue';
import SvgBlankState from '../components/blank_state.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
export default {
components: {
pipelinesTableComponent,
errorState,
emptyState,
loadingIcon,
},
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
PipelinesTableComponent,
SvgBlankState,
EmptyState,
LoadingIcon,
},
data() {
return {
......@@ -85,6 +81,7 @@ export default {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
this.hasMadeRequest = true;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
......@@ -96,7 +93,7 @@ export default {
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occurred while making the request.'));
.catch(() => Flash(__('An error occurred while making the request.')));
},
},
};
......@@ -130,7 +130,7 @@ const bindEvents = () => {
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
$('.import_git').on('click', () => {
$('.js-import-git-toggle-button').on('click', () => {
const $projectMirror = $('#project_mirror');
$projectMirror.attr('disabled', !$projectMirror.attr('disabled'));
......
......@@ -88,7 +88,7 @@ export default {
</script>
<template>
<div class="block labels">
<div class="block labels js-labels-block">
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
......@@ -104,7 +104,7 @@ export default {
</dropdown-value>
<div
v-if="canEdit"
class="selectbox"
class="selectbox js-selectbox"
style="display: none;"
>
<dropdown-hidden-input
......
......@@ -35,7 +35,7 @@ export default {
</script>
<template>
<div class="hide-collapsed value issuable-show-labels">
<div class="hide-collapsed value issuable-show-labels js-value">
<span
v-if="isEmpty"
class="text-secondary"
......
......@@ -9,7 +9,6 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
......
module Boards
class IssuesController < Boards::ApplicationController
prepend EE::BoardsResponses
prepend EE::Boards::IssuesController
include BoardsResponses
include ControllerWithCrossProjectAccessCheck
requires_cross_project_access if: -> { board&.group_board? }
before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index]
......@@ -66,11 +67,19 @@ module Boards
end
def issues_finder
IssuesFinder.new(current_user, project_id: board_parent.id)
if board.group_board?
IssuesFinder.new(current_user, group_id: board_parent.id)
else
IssuesFinder.new(current_user, project_id: board_parent.id)
end
end
def project
board_parent
@project ||= if board.group_board?
Project.find(issue_params[:project_id])
else
board_parent
end
end
def move_params
......
module Boards
class ListsController < Boards::ApplicationController
prepend EE::BoardsResponses
include BoardsResponses
before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate]
......
module BoardsResponses
include Gitlab::Utils::StrongMemoize
def board_params
params.require(:board).permit(:name, :weight, :milestone_id, :assignee_id, label_ids: [])
end
def parent
strong_memoize(:parent) do
group? ? group : project
end
end
def boards_path
if group?
group_boards_path(parent)
else
project_boards_path(parent)
end
end
def board_path(board)
if group?
group_board_path(parent, board)
else
project_board_path(parent, board)
end
end
def group?
instance_variable_defined?(:@group)
end
def authorize_read_list
authorize_action_for!(board.parent, :read_list)
ability = board.group_board? ? :read_group : :read_list
authorize_action_for!(board.parent, ability)
end
def authorize_read_issue
authorize_action_for!(board.parent, :read_issue)
ability = board.group_board? ? :read_group : :read_issue
authorize_action_for!(board.parent, ability)
end
def authorize_update_issue
......@@ -31,6 +67,10 @@ module BoardsResponses
respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
def respond_with(resource)
respond_to do |format|
format.html
......
class Groups::BoardsController < Groups::ApplicationController
prepend EE::Boards::BoardsController
prepend EE::BoardsResponses
include BoardsResponses
before_action :check_group_issue_boards_available!
before_action :assign_endpoint_vars
def index
......@@ -23,4 +21,8 @@ class Groups::BoardsController < Groups::ApplicationController
@namespace_path = group.to_param
@labels_endpoint = group_labels_url(group)
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
end
......@@ -62,7 +62,7 @@ class InvitesController < ApplicationController
case source
when Project
project = member.source
label = "project #{project.name_with_namespace}"
label = "project #{project.full_name}"
path = project_path(project)
when Group
group = member.source
......
......@@ -38,7 +38,7 @@ class Projects::BlobController < Projects::ApplicationController
end
format.json do
page_title @blob.path, @ref, @project.name_with_namespace
page_title @blob.path, @ref, @project.full_name
show_json
end
......
class Projects::BoardsController < Projects::ApplicationController
prepend EE::Boards::BoardsController
prepend EE::BoardsResponses
include BoardsResponses
include IssuableCollections
......
......@@ -7,13 +7,19 @@ class Projects::BranchesController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
@sort = params[:sort].presence || sort_value_recently_updated
@branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
# Support legacy URLs
before_action :redirect_for_legacy_index_sort_or_search, only: [:index]
def index
respond_to do |format|
format.html do
@sort = params[:sort].presence || sort_value_recently_updated
@mode = params[:state].presence || 'overview'
@overview_max_branches = 5
# Fetch branches for the specified mode
fetch_branches_by_mode
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names =
repository.merged_branch_names(@branches.map(&:name))
......@@ -28,7 +34,9 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
format.json do
render json: @branches.map(&:name)
branches = BranchesFinder.new(@repository, params).execute
branches = Kaminari.paginate_array(branches).page(params[:page])
render json: branches.map(&:name)
end
end
end
......@@ -123,4 +131,27 @@ class Projects::BranchesController < Projects::ApplicationController
context: 'autodeploy'
)
end
def redirect_for_legacy_index_sort_or_search
# Normalize a legacy URL with redirect
if request.format != :json && !params[:state].presence && [:sort, :search, :page].any? { |key| params[key].presence }
redirect_to project_branches_filtered_path(@project, state: 'all'), notice: 'Update your bookmarked URLs as filtered/sorted branches URL has been changed.'
end
end
def fetch_branches_by_mode
if @mode == 'overview'
# overview mode
@active_branches, @stale_branches = BranchesFinder.new(@repository, sort: sort_value_recently_updated).execute.partition(&:active?)
# Here we get one more branch to indicate if there are more data we're not showing
@active_branches = @active_branches.first(@overview_max_branches + 1)
@stale_branches = @stale_branches.first(@overview_max_branches + 1)
@branches = @active_branches + @stale_branches
else
# active/stale/all view mode
@branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
@branches = @branches.select { |b| b.state.to_s == @mode } if %w[active stale].include?(@mode)
@branches = Kaminari.paginate_array(@branches).page(params[:page])
end
end
end
......@@ -17,10 +17,8 @@ class Projects::CompareController < Projects::ApplicationController
def show
apply_diff_view_cookie!
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
render
end
def diff_for_path
......
......@@ -9,25 +9,22 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
def show
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
Gitlab::GitalyClient.allow_n_plus_1_calls do
@url = project_network_path(@project, @ref, @options.merge(format: :json))
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
respond_to do |format|
format.html do
if @options[:extended_sha1] && !@commit
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
end
end
@url = project_network_path(@project, @ref, @options.merge(format: :json))
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
format.json do
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
respond_to do |format|
format.html do
if @options[:extended_sha1] && !@commit
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
end
end
render
format.json do
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
end
end
render
end
def assign_commit
......
......@@ -36,7 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
end
format.json do
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
page_title @path.presence || _("Files"), @ref, @project.full_name
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do
......
......@@ -43,11 +43,11 @@ class ProjectsController < Projects::ApplicationController
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
redirect_to(
project_path(@project),
project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
render 'new', locals: { active_tab: active_new_project_tab }
end
end
......@@ -105,7 +105,7 @@ class ProjectsController < Projects::ApplicationController
def show
if @project.import_in_progress?
redirect_to project_import_path(@project)
redirect_to project_import_path(@project, custom_import_params)
return
end
......@@ -132,7 +132,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
redirect_to dashboard_projects_path, status: 302
rescue Projects::DestroyService::DestroyError => ex
......@@ -361,6 +361,14 @@ class ProjectsController < Projects::ApplicationController
]
end
def custom_import_params
{}
end
def active_new_project_tab
project_params[:import_url].present? ? 'import' : 'blank'
end
def repo_exists?
project.repository_exists? && !project.empty_repo?
......
class BranchesFinder
def initialize(repository, params)
def initialize(repository, params = {})
@repository = repository
@params = params
end
......
......@@ -19,6 +19,10 @@
# non_archived: boolean
# iids: integer[]
# my_reaction_emoji: string
# created_after: datetime
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
......@@ -79,6 +83,7 @@ class IssuableFinder
def filter_items(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
items = by_state(items)
items = by_group(items)
items = by_search(items)
......@@ -283,6 +288,13 @@ class IssuableFinder
end
end
def by_updated_at(items)
items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
items
end
def by_state(items)
case params[:state].to_s
when 'closed'
......
......@@ -17,6 +17,10 @@
# my_reaction_emoji: string
# public_only: boolean
# due_date: date or '0', '', 'overdue', 'week', or 'month'
# created_after: datetime
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
#
class IssuesFinder < IssuableFinder
prepend EE::IssuesFinder
......
......@@ -19,6 +19,10 @@
# my_reaction_emoji: string
# source_branch: string
# target_branch: string
# created_after: datetime
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
def klass
......
......@@ -110,10 +110,6 @@ class TodosFinder
ids
end
def projects(items)
ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute
end
def type?
type.present? && %w(Issue MergeRequest).include?(type)
end
......@@ -152,13 +148,12 @@ class TodosFinder
def by_project(items)
if project?
items = items.where(project: project)
items.where(project: project)
else
item_projects = projects(items)
items = items.merge(item_projects).joins(:project)
end
projects = Project.public_or_visible_to_user(current_user)
items
items.joins(:project).merge(projects)
end
end
def by_state(items)
......
......@@ -19,23 +19,35 @@ module BoardsHelper
end
def build_issue_link_base
project_issues_path(@project)
if board.group_board?
"#{group_path(@board.group)}/:project_path/issues"
else
project_issues_path(@project)
end
end
def board_base_url
project_boards_path(@project)
if board.group_board?
group_boards_url(@group)
else
project_boards_path(@project)
end
end
def multiple_boards_available?
current_board_parent.multiple_issue_boards_available?(current_user)
current_board_parent.multiple_issue_boards_available?
end
def current_board_path(board)
@current_board_path ||= project_board_path(current_board_parent, board)
@current_board_path ||= if board.group_board?
group_board_path(current_board_parent, board)
else
project_board_path(current_board_parent, board)
end
end
def current_board_parent
@current_board_parent ||= @project
@current_board_parent ||= @group || @project
end
def can_admin_issue?
......@@ -49,7 +61,8 @@ module BoardsHelper
labels: labels_filter_path(true),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.try(:path)
project_path: @project&.path,
group_path: @group&.path
}
end
......@@ -61,7 +74,8 @@ module BoardsHelper
field_name: 'issue[assignee_ids][]',
first_user: current_user&.username,
current_user: 'true',
project_id: @project&.try(:id),
project_id: @project&.id,
group_id: @group&.id,
null_user: 'true',
multi_select: 'true',
'dropdown-header': dropdown_options[:data][:'dropdown-header'],
......
module BranchesHelper
prepend EE::BranchesHelper
def filter_branches_path(options = {})
exist_opts = {
search: params[:search],
sort: params[:sort]
}
options = exist_opts.merge(options)
project_branches_path(@project, @id, options)
end
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
......
......@@ -30,7 +30,7 @@ module FormHelper
null_user: true,
current_user: true,
project_id: @project&.id,
field_name: "issue[assignee_ids][]",
field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
......
......@@ -137,7 +137,7 @@ module GroupsHelper
links = [:overview, :group_members]
if can?(current_user, :read_cross_project)
links += [:activity, :issues, :labels, :milestones, :merge_requests]
links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
end
if can?(current_user, :admin_group, @group)
......
module ImportHelper
def has_ci_cd_only_params?
false
end
def import_project_target(owner, name)
namespace = current_user.can_create_group? ? owner : current_user.namespace_path
"#{namespace}/#{name}"
end
def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend
def provider_project_link(provider, full_path)
url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend
link_to full_path, url, target: '_blank', rel: 'noopener noreferrer'
end
def import_will_timeout_message(_ci_cd_only)
timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)
_('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout }
end
def import_svn_message(_ci_cd_only)
svn_link = link_to _('this document'), help_page_path('user/project/import/svn')
_('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link }
end
def import_in_progress_title
if @project.forked?
_('Forking in progress')
else
_('Import in progress')
end
end
link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
def import_wait_and_refresh_message
_('Please wait while we import the repository for you. Refresh at will.')
end
private
def github_project_url(path_with_namespace)
"#{github_root_url}/#{path_with_namespace}"
def github_project_url(full_path)
"#{github_root_url}/#{full_path}"
end
def github_root_url
......@@ -23,7 +49,7 @@ module ImportHelper
@github_url = provider.fetch('url', 'https://github.com') if provider
end
def gitea_project_url(path_with_namespace)
"#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
def gitea_project_url(full_path)
"#{@gitea_host_url.sub(%r{/+\z}, '')}/#{full_path}"
end
end
......@@ -99,7 +99,7 @@ module IssuablesHelper
project = Project.find_by(id: project_id)
if project
project.name_with_namespace
project.full_name
else
default_label
end
......
......@@ -11,7 +11,7 @@ module NotesHelper
end
def note_supports_quick_actions?(note)
Notes::QuickActionsService.supported?(note, current_user)
Notes::QuickActionsService.supported?(note)
end
def noteable_json(noteable)
......
......@@ -99,13 +99,13 @@ module ProjectsHelper
end
def remove_project_message(project)
_("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace }
_("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
end
def transfer_project_message(project)
_("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace }
_("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
end
def remove_fork_project_message(project)
......
......@@ -120,7 +120,7 @@ module SearchHelper
category: "Projects",
id: p.id,
value: "#{search_result_sanitize(p.name)}",
label: "#{search_result_sanitize(p.name_with_namespace)}",
label: "#{search_result_sanitize(p.full_name)}",
url: project_path(p)
}
end
......
......@@ -114,7 +114,7 @@ module TodosHelper
projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.map do |project|
{ id: project.id, text: project.name_with_namespace }
{ id: project.id, text: project.full_name }
end
projects.unshift({ id: '', text: 'Any Project' }).to_json
......
......@@ -38,5 +38,13 @@ module Emails
reply_to: @message.reply_to,
subject: @message.subject)
end
def mirror_was_hard_failed_email(project_id, user_id)
@project = Project.find(project_id)
user = User.find(user_id)
mail(to: user.notification_email,
subject: subject('Repository mirroring paused'))
end
end
end
class Board < ActiveRecord::Base
prepend EE::Board
belongs_to :group
belongs_to :project
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :project, presence: true, if: :project_needed?
validates :group, presence: true, unless: :project
def project_needed?
true
!group
end
def parent
project
@parent ||= group || project
end
def group_board?
false
group_id.present?
end
def backlog_list
......
......@@ -56,12 +56,13 @@ module Clusters
def specification
{
"gitlabUrl" => gitlab_url,
"runnerToken" => ensure_runner.token
"runnerToken" => ensure_runner.token,
"runners" => { "privileged" => privileged }
}
end
def content_values
specification.merge(YAML.load_file(chart_values_file))
YAML.load_file(chart_values_file).deep_merge!(specification)
end
end
end
......
......@@ -19,6 +19,7 @@ module Issuable
include AfterCommitQueue
include Sortable
include CreatedAtFilterable
include UpdatedAtFilterable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
......
module UpdatedAtFilterable
extend ActiveSupport::Concern
included do
scope :updated_before, ->(date) { where(scoped_table[:updated_at].lteq(date)) }
scope :updated_after, ->(date) { where(scoped_table[:updated_at].gteq(date)) }
def self.scoped_table
arel_table.alias(table_name)
end
end
end
......@@ -164,7 +164,7 @@ class Event < ActiveRecord::Base
def project_name
if project
project.name_with_namespace
project.full_name
else
"(deleted project)"
end
......
......@@ -36,6 +36,8 @@ class Group < Namespace
has_many :hooks, dependent: :destroy, class_name: 'GroupHook' # rubocop:disable Cop/ActiveRecordDependent
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :boards
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id'
......
class Label < ActiveRecord::Base
# EE specific
prepend EE::Label
include CacheMarkdownField
include Referable
include Subscribable
......@@ -38,6 +35,7 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
def self.prioritized(project)
......
......@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
def commits_count
super || merge_request_diff_commits.size
end
private
def create_merge_request_diff_files(diffs)
......
......@@ -232,9 +232,9 @@ class Namespace < ActiveRecord::Base
has_parent?
end
## EE only
def multiple_issue_boards_available?(user = nil)
feature_available?(:multiple_issue_boards)
# Overridden on EE module
def multiple_issue_boards_available?
false
end
def full_path_was
......
......@@ -24,12 +24,7 @@ module Network
end
def parents(map)
@commit.parents.map do |p|
if map.include?(p.id)
map[p.id]
end
end
.compact
map.values_at(*@commit.parent_ids).compact
end
end
end
......@@ -1539,16 +1539,34 @@ class Project < ActiveRecord::Base
end
end
def import_export_shared
@import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
end
def export_path
return nil unless namespace.present? || hashed_storage?(:repository)
File.join(Gitlab::ImportExport.storage_path, disk_path)
import_export_shared.archive_path
end
def export_project_path
Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
end
def export_status
if export_in_progress?
:started
elsif export_project_path
:finished
else
:none
end
end
def export_in_progress?
import_export_shared.active_export_count > 0
end
def remove_exports
return nil unless export_path.present?
......@@ -1675,8 +1693,9 @@ class Project < ActiveRecord::Base
end
end
def multiple_issue_boards_available?(user)
feature_available?(:multiple_issue_boards, user)
# Overridden on EE module
def multiple_issue_boards_available?
false
end
def full_path_was
......
......@@ -68,7 +68,7 @@ http://app.asana.com/-/account_api'
end
user = data[:user_name]
project_name = project.name_with_namespace
project_name = project.full_name
data[:commits].each do |commit|
push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
......
......@@ -86,7 +86,7 @@ class CampfireService < Service
after = push[:after]
message = ""
message << "[#{project.name_with_namespace}] "
message << "[#{project.full_name}] "
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
......
......@@ -129,7 +129,7 @@ class ChatNotificationService < Service
end
def project_name
project.name_with_namespace.gsub(/\s/, '')
project.full_name.gsub(/\s/, '')
end
def project_url
......
......@@ -120,7 +120,7 @@ class HipchatService < Service
else
message << "pushed to #{ref_type} <a href=\""\
"#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/, '')}</a> "
message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
push[:commits].take(MAX_COMMITS).each do |commit|
......@@ -275,7 +275,7 @@ class HipchatService < Service
end
def project_name
project.name_with_namespace.gsub(/\s/, '')
project.full_name.gsub(/\s/, '')
end
def project_url
......
class JiraService < IssueTrackerService
include Gitlab::Routing
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
......@@ -268,7 +270,9 @@ class JiraService < IssueTrackerService
url: url,
title: title,
status: status,
icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
icon: {
title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
}
}
}
end
......
......@@ -37,7 +37,7 @@ class MattermostSlashCommandsService < SlashCommandsService
private
def command(params)
pretty_project_name = project.name_with_namespace
pretty_project_name = project.full_name
params.merge(
auto_complete: true,
......
......@@ -88,10 +88,10 @@ class PushoverService < Service
user: user_key,
device: device,
priority: priority,
title: "#{project.name_with_namespace}",
title: "#{project.full_name}",
message: message,
url: data[:project][:web_url],
url_title: "See project #{project.name_with_namespace}"
url_title: "See project #{project.full_name}"
}
# Sound parameter MUST NOT be sent to API if not selected
......
......@@ -260,7 +260,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
return unless sha && commit_by(oid: sha)
return unless sha.present? && commit_by(oid: sha)
return if kept_around?(sha)
......
......@@ -32,8 +32,6 @@ class Todo < ActiveRecord::Base
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
default_scope { reorder(id: :desc) }
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
......@@ -53,10 +51,14 @@ class Todo < ActiveRecord::Base
# milestones, but still show something if the user has a URL with that
# selected.
def sort(method)
case method.to_s
when 'priority', 'label_priority' then order_by_labels_priority
else order_by(method)
end
sorted =
case method.to_s
when 'priority', 'label_priority' then order_by_labels_priority
else order_by(method)
end
# Break ties with the ID column for pagination
sorted.order(id: :desc)
end
# Order by priority depending on which issue/merge request the Todo belongs to
......
......@@ -51,7 +51,12 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
rule { reporter }.policy do
enable :admin_label
enable :admin_list
enable :admin_issue
end
rule { master }.policy do
enable :create_projects
......
......@@ -41,7 +41,11 @@ module Boards
end
def set_parent
params[:project_id] = parent.id
if parent.is_a?(Group)
params[:group_id] = parent.id
else
params[:project_id] = parent.id
end
end
def set_state
......
module Boards
module Issues
class MoveService < Boards::BaseService
prepend EE::Boards::Issues::MoveService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false if issue_params.empty?
......@@ -62,8 +60,10 @@ module Boards
label_ids =
if moving_to_list.movable?
moving_from_list.label_id
elsif board.group_board?
::Label.on_group_boards(parent.id).pluck(:label_id)
else
Label.on_project_boards(parent.id).pluck(:label_id)
::Label.on_project_boards(parent.id).pluck(:label_id)
end
Array(label_ids).compact
......
module Boards
module Lists
class CreateService < Boards::BaseService
prepend EE::Boards::Lists::CreateService
def execute(board)
List.transaction do
label = available_labels_for(board).find(params[:label_id])
......@@ -14,7 +12,11 @@ module Boards
private
def available_labels_for(board)
LabelsFinder.new(current_user, project_id: parent.id).execute
if board.group_board?
parent.labels
else
LabelsFinder.new(current_user, project_id: parent.id).execute
end
end
def next_position(board)
......
module Ci
class CreateTraceArtifactService < BaseService
def execute(job)
return if job.job_artifacts_trace
job.trace.read do |stream|
break unless stream.file?
clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
create_job_trace!(job, clone_path)
FileUtils.rm(stream.path)
end
end
end
private
def create_job_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!(
project: job.project,
file_type: :trace,
file: stream)
end
end
def clone_file!(src_path, temp_dir)
FileUtils.mkdir_p(temp_dir)
Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
temp_path = File.join(dir_path, "job.log")
FileUtils.copy(src_path, temp_path)
yield(temp_path)
end
end
end
end
module MergeRequests
class UpdateService < MergeRequests::BaseService
prepend ::EE::MergeRequests::UpdateService
def execute(merge_request)
# We don't allow change of source/target projects and source branch
# after merge request was created
......
......@@ -9,14 +9,12 @@ module Notes
UPDATE_SERVICES[note.noteable_type]
end
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
def self.supported?(note)
!!noteable_update_service(note)
end
def supported?(note)
self.class.supported?(note, current_user)
self.class.supported?(note)
end
def extract_commands(note, options = {})
......
......@@ -2,7 +2,7 @@ module Projects
module ImportExport
class ExportService < BaseService
def execute(_options = {})
@shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.disk_path, 'work'))
@shared = project.import_export_shared
save_all
end
......
module Projects
class UpdatePagesService < BaseService
InvaildStateError = Class.new(StandardError)
FailedToExtractError = Class.new(StandardError)
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
SITE_PATH = 'public/'.freeze
......@@ -11,13 +14,15 @@ module Projects
end
def execute
register_attempt
# Create status notifying the deployment of pages
@status = create_status
@status.enqueue!
@status.run!
raise 'missing pages artifacts' unless build.artifacts?
raise 'pages are outdated' unless latest?
raise InvaildStateError, 'missing pages artifacts' unless build.artifacts?
raise InvaildStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
FileUtils.mkdir_p(tmp_path)
......@@ -26,24 +31,22 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public')
raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise 'pages are outdated' unless latest?
raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise InvaildStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path)
success
end
rescue => e
rescue InvaildStateError, FailedToExtractError => e
register_failure
error(e.message)
ensure
register_attempt
build.erase_artifacts! unless build.has_expiring_artifacts?
end
private
def success
@status.success
delete_artifact!
super
end
......@@ -52,6 +55,7 @@ module Projects
@status.allow_failure = !latest?
@status.description = message
@status.drop(:script_failure)
delete_artifact!
super
end
......@@ -72,7 +76,7 @@ module Projects
elsif artifacts_filename.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
raise 'unsupported artifacts format'
raise FailedToExtractError, 'unsupported artifacts format'
end
end
......@@ -82,18 +86,18 @@ module Projects
%W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
%W(tar -x -C #{temp_path} #{SITE_PATH}),
err: '/dev/null')
raise 'pages failed to extract' unless results.compact.all?(&:success?)
raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
end
end
def extract_zip_archive!(temp_path)
raise 'missing artifacts metadata' unless build.artifacts_metadata?
raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size
raise "artifacts for pages are too large: #{public_entry.total_size}"
raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}"
end
# Requires UnZip at least 6.00 Info-ZIP.
......@@ -103,7 +107,7 @@ module Projects
site_path = File.join(SITE_PATH, '*')
build.artifacts_file.use_file do |artifacts_path|
unless system(*%W(unzip -qq -n #{artifacts_path} #{site_path} -d #{temp_path}))
raise 'pages failed to extract'
raise FailedToExtractError, 'pages failed to extract'
end
end
end
......@@ -167,6 +171,11 @@ module Projects
build.ref
end
def delete_artifact!
build.reload # Reload stable object to prevent erase artifacts with old state
build.erase_artifacts! unless build.has_expiring_artifacts?
end
def latest_sha
project.commit(build.ref).try(:sha).to_s
end
......
......@@ -22,8 +22,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
created_at: model.created_at.xmlschema,
updated_at: model.updated_at.xmlschema
created_at: model.created_at&.xmlschema,
updated_at: model.updated_at&.xmlschema
}
case model
......
......@@ -190,7 +190,7 @@
%h4 Latest projects
- @projects.each do |project|
%p
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
%span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
......
......@@ -82,7 +82,7 @@
- @projects.each do |project|
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
......@@ -100,7 +100,7 @@
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
......
- add_to_breadcrumbs "Projects", admin_projects_path
- breadcrumb_title @project.name_with_namespace
- page_title @project.name_with_namespace, "Projects"
- breadcrumb_title @project.full_name
- page_title @project.full_name, "Projects"
%h3.page-title
Project: #{@project.name_with_namespace}
Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
......
......@@ -39,7 +39,7 @@
%tr.alert-info
%td
%strong
= project.name_with_namespace
= project.full_name
%td
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
......@@ -61,7 +61,7 @@
- @projects.each do |project|
%tr
%td
= project.name_with_namespace
= project.full_name
%td
.pull-right
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
......@@ -95,7 +95,7 @@
%td.status
- if project
= project.name_with_namespace
= project.full_name
%td.build-link
- if project
......
......@@ -29,12 +29,12 @@
.panel.panel-default
.panel-heading Joined projects (#{@joined_projects.count})
%ul.well-list
- @joined_projects.sort_by(&:name_with_namespace).each do |project|
- @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
.list-item-name
= link_to admin_project_path(project), class: dom_class(project) do
= project.name_with_namespace
= project.full_name
- if member
.pull-right
......
- page_title 'Labels'
- issuables = ['issues', 'merge requests'] + (@group&.feature_available?(:epics) ? ['epics'] : [])
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
= _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
.nav-controls
- if can?(current_user, :admin_label, @group)
......@@ -16,4 +18,4 @@
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
No labels created yet.
= _("No labels created yet.")
......@@ -14,7 +14,7 @@
.list-item-name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
%strong= link_to project.name_with_namespace, project
%strong= link_to project.full_name, project
.pull-right
- if project.archived
%span.label.label-warning archived
......
......@@ -12,7 +12,7 @@
- project = @member.source
project
%strong
= link_to project.name_with_namespace, project_url(project)
= link_to project.full_name, project_url(project)
- when Group
- group = @member.source
group
......
- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
......
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
......@@ -62,6 +59,7 @@
%strong.fly-out-top-item-name
#{ _('Issues') }
%span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do
......@@ -70,9 +68,9 @@
- if group_sidebar_link?(:boards)
= nav_link(path: ['boards#index', 'boards#show']) do
= link_to group_boards_path(@group), title: 'Boards' do
= link_to group_boards_path(@group), title: boards_link_text do
%span
Boards
= boards_link_text
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
......
- page_title @project.name_with_namespace
- page_title @project.full_name
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
- nav "project"
......
......@@ -3,6 +3,6 @@
%p
The project export can be downloaded from:
= link_to download_export_project_url(@project), rel: 'nofollow', download: '' do
= @project.name_with_namespace + " export"
= @project.full_name + " export"
%p
The download link will expire in 24 hours.
......@@ -3,7 +3,7 @@
%p
The project is now located under
= link_to project_url(@project) do
= @project.name_with_namespace
= @project.full_name
%p
To update the remote url in your local repository run (for ssh):
%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
......
......@@ -4,7 +4,7 @@
%td
%strong
- if can?(current_user, :read_project, project)
= link_to project.name_with_namespace, project_path(project)
= link_to project.full_name, project_path(project)
- else
.light N/A
%td
......
......@@ -12,7 +12,9 @@
Add an SSH key
%p.profile-settings-content
Before you can add an SSH key you need to
= link_to "generate it.", help_page_path("ssh/README")
= link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
or use an
= link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
= render 'form'
%hr
%h5
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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