Commit 663593e8 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into no-ivar-in-modules

* upstream/master: (126 commits)
  Update VERSION to 10.3.0-pre
  Update CHANGELOG.md for 10.2.0
  default fill color for SVGs
  ignore hashed repos (for now) when using `rake gitlab:cleanup:repos`
  Use Redis cache for branch existence checks
  Update CONTRIBUTING.md: Link definition of done to criteria
  Use `make install` for Gitaly setups in non-test environments
  FileUploader should check for hashed_storage?(:attachments) to use disk_path
  Set the default gitlab-shell timeout to 3 hours
  Update composite pipelines index to include "id"
  Use arrays in Pipeline#latest_builds_with_artifacts
  Fix blank states using old css
  Skip confirmation user api
  Custom issue tracker
  Revert "check for `read_only?` first before seeing if request is disallowed"
  add `#with_metadata` scope to remove a N+1 from the notes' API
  Fix promoting milestone updating all issuables without milestone
  Batchload blobs for diff generation
  check for `read_only?` first before seeing if request is disallowed
  use `Gitlab::Routing.url_helpers` instead of `Rails.application.routes.url_helpers`
  ...
parents 166a2d7a 6369db01
......@@ -193,7 +193,7 @@ review-docs-deploy:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- ./trigger-build-docs deploy
......@@ -256,7 +256,7 @@ flaky-examples-check:
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
allow_failure: yes
allow_failure: true
retry: 0
only:
- branches
......@@ -416,7 +416,6 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
allow_failure: no
retry: 0
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
......@@ -475,7 +474,7 @@ migration:path-mysql:
<<: *pull-cache
stage: test
script:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:rollback STEP=119
- bundle exec rake db:migrate
db:rollback-pg:
......
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
filtered by the "feature proposal" label:
For the Community Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal
and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate.
------
### Description
(Include problem, use cases, benefits, and/or goals)
......@@ -25,26 +6,4 @@ Please remove this notice if you're confident your issue isn't a duplicate.
### Links / references
### Documentation blurb
#### Overview
What is it?
Why should someone use this feature?
What is the underlying (business) problem?
How do you use this feature?
#### Use cases
Who is this for? Provide one or more use cases.
### Feature checklist
Make sure these are completed before closing the issue,
with a link to the relevant commit.
- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance)
- [ ] Documentation
- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
/label ~"feature proposal"
\ No newline at end of file
/label ~"feature proposal"
This diff is collapsed.
......@@ -543,6 +543,7 @@ When having your code reviewed and when reviewing merge requests please take the
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
1. The merge request meets the [definition of done](#definition-of-done).
## Definition of done
......
......@@ -263,6 +263,8 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader'
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
......@@ -398,7 +400,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -73,6 +73,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.1.1)
bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
......@@ -275,7 +276,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.52.0)
gitaly-proto (0.54.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -355,10 +356,10 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.6.6)
grpc (1.7.2)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1)
googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
tilt
haml_lint (0.26.0)
......@@ -982,6 +983,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
batch-loader
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
......@@ -1034,7 +1036,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.52.0)
gitaly-proto (~> 0.54.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......
10.2.0-pre
10.3.0-pre
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
/* global NewCommitForm */
import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
......@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
import './services/board_service';
import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
......@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
......@@ -112,6 +118,46 @@ $(() => {
methods: {
updateTokens() {
this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.json())
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({
subscribed: data.subscribed,
});
})
.catch(() => {
newIssue.setFetchingState('subscriptions', false);
Flash(__('An error occurred while fetching sidebar data'));
});
}
Store.detail.issue = newIssue;
},
clearDetailIssue() {
Store.detail.issue = {};
},
toggleSubscription(id) {
const issue = Store.detail.issue;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
.then(() => {
issue.setFetchingState('subscriptions', false);
issue.updateData({
subscribed: !issue.subscribed,
});
})
.catch(() => {
issue.setFetchingState('subscriptions', false);
Flash(__('An error occurred when toggling the notification subscription'));
});
}
}
},
});
......
<script>
import './issue_card_inner';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardsIssueCard',
template: `
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:update-filters="true" />
</li>
`,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
......@@ -56,12 +42,30 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
eventHub.$emit('clearDetailIssue');
} else {
Store.detail.issue = this.issue;
eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
}
},
},
};
</script>
<template>
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:update-filters="true" />
</li>
</template>
/* global Sortable */
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
......@@ -5,12 +5,13 @@
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
......@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
},
components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle,
assignees: Assignees,
subscriptions,
},
});
......@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
this.isFetching = {
subscriptions: true,
};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
......@@ -73,6 +78,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
updateData(newData) {
Object.assign(this, newData);
}
setFetchingState(key, value) {
this.isFetching[key] = value;
}
update (url) {
const data = {
issue: {
......
......@@ -2,7 +2,7 @@
import Vue from 'vue';
class BoardService {
export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
......@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
}
}
window.BoardService = BoardService;
import axios from 'axios';
import setAxiosCsrfToken from '../../lib/utils/axios_utils';
import axios from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
setAxiosCsrfToken();
this.options = options;
this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint,
......@@ -18,7 +15,6 @@ export default class ClusterService {
}
installApplication(appId) {
const endpoint = this.appInstallEndpointMap[appId];
return axios.post(endpoint);
return axios.post(this.appInstallEndpointMap[appId]);
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import { s__ } from './locale';
/* global ProjectSelect */
import projectSelect from './project_select';
import IssuableIndex from './issuable_index';
/* global Milestone */
import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global NewBranchForm */
import NewBranchForm from './new_branch_form';
/* global NotificationsForm */
/* global NotificationsDropdown */
import groupAvatar from './group_avatar';
......@@ -18,16 +18,14 @@ import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
import NewCommitForm from './new_commit_form';
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
/* global ProjectNew */
/* global ProjectShow */
import ProjectNew from './project_new';
import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
......@@ -91,6 +89,8 @@ import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription';
import ProjectVariables from './project_variables';
(function() {
var Dispatcher;
......@@ -187,7 +187,7 @@ import Diff from './diff';
initIssuableSidebar();
break;
case 'dashboard:milestones:index':
new ProjectSelect();
projectSelect();
break;
case 'projects:milestones:show':
case 'groups:milestones:show':
......@@ -197,7 +197,7 @@ import Diff from './diff';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
new ProjectSelect();
projectSelect();
initLegacyFilters();
break;
case 'groups:issues':
......@@ -206,7 +206,7 @@ import Diff from './diff';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
new ProjectSelect();
projectSelect();
break;
case 'dashboard:todos:index':
new Todos();
......@@ -339,7 +339,8 @@ import Diff from './diff';
container: '.js-commit-pipeline-graph',
}).bindEvents();
initNotes();
initChangesDropdown();
const stickyBarPaddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:commit:pipelines':
......@@ -484,7 +485,7 @@ import Diff from './diff';
if ($el.find('.dropdown-group-label').length) {
new GroupLabelSubscription($el);
} else {
new gl.ProjectLabelSubscription($el);
new ProjectLabelSubscription($el);
}
});
break;
......@@ -520,7 +521,7 @@ import Diff from './diff';
// Initialize expandable settings panels
initSettingsPanels();
case 'groups:settings:ci_cd:show':
new gl.ProjectVariables();
new ProjectVariables();
break;
case 'ci:lints:create':
case 'ci:lints:show':
......@@ -623,7 +624,6 @@ import Diff from './diff';
case 'show':
new Star();
new ProjectNew();
new ProjectShow();
new NotificationsDropdown();
break;
case 'wikis':
......
......@@ -227,25 +227,27 @@ export default {
/>
<div
class="blank-state blank-state-no-icon"
class="blank-state-row"
v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New environment
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New environment
</a>
</div>
</div>
<div
......
......@@ -14,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
/* eslint-disable no-new */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global SubscriptionSelect */
import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
......@@ -11,5 +10,5 @@ export default () => {
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
new SubscriptionSelect();
subscriptionSelect();
};
/* eslint-disable class-methods-use-this, no-new */
/* global MilestoneSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import issueStatusSelect from './issue_status_select';
import './subscription_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
......@@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
new SubscriptionSelect();
subscriptionSelect();
}
setupBulkUpdateActions() {
......
......@@ -102,6 +102,11 @@ export default {
required: false,
default: 'issue',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
data() {
const store = new Store({
......@@ -234,6 +239,7 @@ export default {
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
/>
<div v-else>
<title-component
......
......@@ -17,6 +17,11 @@
type: String,
required: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
components: {
markdownField,
......@@ -36,7 +41,8 @@
</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath">
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
......
......@@ -41,6 +41,11 @@
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
components: {
lockedWarning,
......@@ -83,7 +88,8 @@
<description-field
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" />
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy"
......
......@@ -29,8 +29,8 @@ export default class JobMediator {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
successCallback: response => this.successCallback(response),
errorCallback: () => this.errorCallback(),
});
if (!Visibility.hidden()) {
......@@ -57,7 +57,7 @@ export default class JobMediator {
successCallback(response) {
this.state.isLoading = false;
return response.json().then(data => this.store.storeJob(data));
return this.store.storeJob(response.data);
}
errorCallback() {
......
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class JobService {
constructor(endpoint) {
this.job = Vue.resource(endpoint);
this.job = endpoint;
}
getJob() {
return this.job.get();
return axios.get(this.job);
}
}
import axios from 'axios';
import csrf from './csrf';
export default function setAxiosCsrfToken() {
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
}
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
// Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb
axios.interceptors.request.use((config) => {
window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1;
return config;
});
// Remove the global counter
axios.interceptors.response.use((config) => {
window.activeVueResources -= 1;
return config;
});
export default axios;
......@@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils';
/**
* Polling utility for handling realtime updates.
* Service for vue resouce and method need to be provided as props
* Requirements: Promise based HTTP client
*
* Service for promise based http client and method need to be provided as props
*
* @example
* new Poll({
......
......@@ -29,7 +29,6 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
import './lib/utils/url_utility';
// behaviors
......@@ -59,11 +58,7 @@ import './line_highlighter';
import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
import './milestone';
import './milestone_select';
import './namespace_select';
import './new_branch_form';
import './new_commit_form';
import './notes';
import './notifications_dropdown';
import './notifications_form';
......@@ -71,11 +66,6 @@ import './pager';
import './preview_markdown';
import './project_find_file';
import './project_import';
import './project_label_subscription';
import './project_new';
import './project_select';
import './project_show';
import './project_variables';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
......@@ -84,9 +74,6 @@ import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Sortable */
import Flash from './flash';
(function() {
this.Milestone = (function() {
function Milestone() {
this.bindTabsSwitching();
export default class Milestone {
constructor() {
this.bindTabsSwitching();
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab();
}
this.loadInitialTab();
}
bindTabsSwitching() {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
Milestone.prototype.bindTabsSwitching = function() {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
location.hash = $target.attr('href');
this.loadTab($target);
});
}
// eslint-disable-next-line class-methods-use-this
loadInitialTab() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
location.hash = $target.attr('href');
this.loadTab($target);
if ($target.length) {
$target.tab('show');
}
}
// eslint-disable-next-line class-methods-use-this
loadTab($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
};
Milestone.prototype.loadInitialTab = function() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
}
};
return Milestone;
})();
}).call(window);
}
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
import RefSelectDropdown from '~/ref_select_dropdown';
import RefSelectDropdown from './ref_select_dropdown';
(function() {
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
this.validate = this.validate.bind(this);
this.branchNameError = form.find('.js-branch-name-error');
this.name = form.find('.js-branch-name');
this.ref = form.find('#ref');
new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
this.setupRestrictions();
this.addBinding();
this.init();
export default class NewBranchForm {
constructor(form, availableRefs) {
this.validate = this.validate.bind(this);
this.branchNameError = form.find('.js-branch-name-error');
this.name = form.find('.js-branch-name');
this.ref = form.find('#ref');
new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
this.setupRestrictions();
this.addBinding();
this.init();
}
addBinding() {
return this.name.on('blur', this.validate);
}
init() {
if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
}
}
NewBranchForm.prototype.addBinding = function() {
return this.name.on('blur', this.validate);
setupRestrictions() {
var endsWith, invalid, single, startsWith;
startsWith = {
pattern: /^(\/|\.)/g,
prefix: "can't start with",
conjunction: "or"
};
NewBranchForm.prototype.init = function() {
if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
}
endsWith = {
pattern: /(\/|\.|\.lock)$/g,
prefix: "can't end in",
conjunction: "or"
};
NewBranchForm.prototype.setupRestrictions = function() {
var endsWith, invalid, single, startsWith;
startsWith = {
pattern: /^(\/|\.)/g,
prefix: "can't start with",
conjunction: "or"
};
endsWith = {
pattern: /(\/|\.|\.lock)$/g,
prefix: "can't end in",
conjunction: "or"
};
invalid = {
pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
prefix: "can't contain",
conjunction: ", "
};
single = {
pattern: /^@+$/g,
prefix: "can't be",
conjunction: "or"
};
return this.restrictions = [startsWith, invalid, endsWith, single];
invalid = {
pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
prefix: "can't contain",
conjunction: ", "
};
single = {
pattern: /^@+$/g,
prefix: "can't be",
conjunction: "or"
};
return this.restrictions = [startsWith, invalid, endsWith, single];
}
NewBranchForm.prototype.validate = function() {
var errorMessage, errors, formatter, unique, validator;
const indexOf = [].indexOf;
validate() {
var errorMessage, errors, formatter, unique, validator;
const indexOf = [].indexOf;
this.branchNameError.empty();
unique = function(values, value) {
if (indexOf.call(values, value) === -1) {
values.push(value);
}
return values;
};
formatter = function(values, restriction) {
var formatted;
formatted = values.map(function(value) {
switch (false) {
case !/\s/.test(value):
return 'spaces';
case !/\/{2,}/g.test(value):
return 'consecutive slashes';
default:
return "'" + value + "'";
}
});
return restriction.prefix + " " + (formatted.join(restriction.conjunction));
};
validator = (function(_this) {
return function(errors, restriction) {
var matched;
matched = _this.name.val().match(restriction.pattern);
if (matched) {
return errors.concat(formatter(matched.reduce(unique, []), restriction));
} else {
return errors;
}
};
})(this);
errors = this.restrictions.reduce(validator, []);
if (errors.length > 0) {
errorMessage = $("<span/>").text(errors.join(', '));
return this.branchNameError.append(errorMessage);
this.branchNameError.empty();
unique = function(values, value) {
if (indexOf.call(values, value) === -1) {
values.push(value);
}
return values;
};
return NewBranchForm;
})();
}).call(window);
formatter = function(values, restriction) {
var formatted;
formatted = values.map(function(value) {
switch (false) {
case !/\s/.test(value):
return 'spaces';
case !/\/{2,}/g.test(value):
return 'consecutive slashes';
default:
return "'" + value + "'";
}
});
return restriction.prefix + " " + (formatted.join(restriction.conjunction));
};
validator = (function(_this) {
return function(errors, restriction) {
var matched;
matched = _this.name.val().match(restriction.pattern);
if (matched) {
return errors.concat(formatter(matched.reduce(unique, []), restriction));
} else {
return errors;
}
};
})(this);
errors = this.restrictions.reduce(validator, []);
if (errors.length > 0) {
errorMessage = $("<span/>").text(errors.join(', '));
return this.branchNameError.append(errorMessage);
}
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
this.NewCommitForm = (function() {
function NewCommitForm(form) {
this.form = form;
this.renderDestination = this.renderDestination.bind(this);
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
export default class NewCommitForm {
constructor(form) {
this.form = form;
this.renderDestination = this.renderDestination.bind(this);
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
NewCommitForm.prototype.renderDestination = function() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
this.createMergeRequest.prop('checked', true);
}
} else {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
renderDestination() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
this.createMergeRequest.prop('checked', true);
}
return this.wasDifferent = different;
};
return NewCommitForm;
})();
}).call(window);
} else {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
}
return this.wasDifferent = different;
}
}
......@@ -267,9 +267,11 @@
/>
<div
class="blank-state blank-state-no-icon"
class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
</div>
<div
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */
import Cookies from 'js-cookie';
import projectSelect from './project_select';
export default class Project {
constructor() {
......@@ -46,7 +46,7 @@ export default class Project {
}
static projectSelectDropdown () {
new ProjectSelect();
projectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
}
......
/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
this.$buttons = this.$container.find('.js-subscribe-button');
(function(global) {
class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
this.$buttons = this.$container.find('.js-subscribe-button');
this.$buttons.on('click', this.toggleSubscription.bind(this));
}
this.$buttons.on('click', this.toggleSubscription.bind(this));
}
toggleSubscription(event) {
event.preventDefault();
toggleSubscription(event) {
event.preventDefault();
const $btn = $(event.currentTarget);
const $span = $btn.find('span');
const url = $btn.attr('data-url');
const oldStatus = $btn.attr('data-status');
const $btn = $(event.currentTarget);
const $span = $btn.find('span');
const url = $btn.attr('data-url');
const oldStatus = $btn.attr('data-status');
$btn.addClass('disabled');
$span.toggleClass('hidden');
$btn.addClass('disabled');
$span.toggleClass('hidden');
$.ajax({
type: 'POST',
url: url
}).done(() => {
let newStatus, newAction;
$.ajax({
type: 'POST',
url,
}).done(() => {
let newStatus;
let newAction;
if (oldStatus === 'unsubscribed') {
[newStatus, newAction] = ['subscribed', 'Unsubscribe'];
} else {
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
}
if (oldStatus === 'unsubscribed') {
[newStatus, newAction] = ['subscribed', 'Unsubscribe'];
} else {
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
}
$span.toggleClass('hidden');
$btn.removeClass('disabled');
$span.toggleClass('hidden');
$btn.removeClass('disabled');
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
this.$buttons.map((button) => {
const $button = $(button);
this.$buttons.map((button) => {
const $button = $(button);
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
}
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
}
return button;
});
return button;
});
}
});
}
global.ProjectLabelSubscription = ProjectLabelSubscription;
})(window.gl || (window.gl = {}));
}
This diff is collapsed.
......@@ -2,79 +2,73 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
(function () {
this.ProjectSelect = (function () {
function ProjectSelect() {
$('.ajax-project-select').each(function(i, select) {
var placeholder;
const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
export default function projectSelect() {
$('.ajax-project-select').each(function(i, select) {
var placeholder;
const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
placeholder = "Search for project";
if (this.includeGroups) {
placeholder += " or group";
}
placeholder = "Search for project";
if (this.includeGroups) {
placeholder += " or group";
}
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function (_this) {
return function (query) {
var finalCallback, projectsCallback;
finalCallback = function (projects) {
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function (_this) {
return function (query) {
var finalCallback, projectsCallback;
finalCallback = function (projects) {
var data;
data = {
results: projects
};
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data;
data = {
results: projects
};
return query.callback(data);
data = groups.concat(projects);
return finalCallback(data);
};
if (_this.includeGroups) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
return Api.groups(query.term, {}, groupsCallback);
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
} else {
projectsCallback = finalCallback;
}
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
if (simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
}
return ProjectSelect;
})();
}).call(window);
},
text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
if (simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
return ProjectShow;
})();
}).call(window);
// I kept class for future
(() => {
const HIDDEN_VALUE_TEXT = '******';
class ProjectVariables {
constructor() {
this.$revealBtn = $('.js-btn-toggle-reveal-values');
this.$revealBtn.on('click', this.toggleRevealState.bind(this));
}
const HIDDEN_VALUE_TEXT = '******';
export default class ProjectVariables {
constructor() {
this.$revealBtn = $('.js-btn-toggle-reveal-values');
this.$revealBtn.on('click', this.toggleRevealState.bind(this));
}
toggleRevealState(e) {
e.preventDefault();
toggleRevealState(e) {
e.preventDefault();
const oldStatus = this.$revealBtn.attr('data-status');
let newStatus = 'hidden';
let newAction = 'Reveal Values';
const oldStatus = this.$revealBtn.attr('data-status');
let newStatus = 'hidden';
let newAction = 'Reveal Values';
if (oldStatus === 'hidden') {
newStatus = 'revealed';
newAction = 'Hide Values';
}
if (oldStatus === 'hidden') {
newStatus = 'revealed';
newAction = 'Hide Values';
}
this.$revealBtn.attr('data-status', newStatus);
this.$revealBtn.attr('data-status', newStatus);
const $variables = $('.variable-value');
const $variables = $('.variable-value');
$variables.each((_, variable) => {
const $variable = $(variable);
let newText = HIDDEN_VALUE_TEXT;
$variables.each((_, variable) => {
const $variable = $(variable);
let newText = HIDDEN_VALUE_TEXT;
if (newStatus === 'revealed') {
newText = $variable.attr('data-value');
}
if (newStatus === 'revealed') {
newText = $variable.attr('data-value');
}
$variable.text(newText);
});
$variable.text(newText);
});
this.$revealBtn.text(newAction);
}
this.$revealBtn.text(newAction);
}
window.gl = window.gl || {};
window.gl.ProjectVariables = ProjectVariables;
})();
}
......@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
export default {
......@@ -21,7 +22,7 @@ export default {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
Flash('Error occurred when toggling the notification subscription');
Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
......
......@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: false,
},
id: {
type: Number,
required: false,
},
},
components: {
loadingButton,
......@@ -32,7 +36,7 @@ export default {
},
methods: {
toggleSubscription() {
eventHub.$emit('toggleSubscription');
eventHub.$emit('toggleSubscription', this.id);
},
},
};
......
class Subscription {
constructor(containerElm) {
this.containerElm = containerElm;
const subscribeButton = containerElm.querySelector('.js-subscribe-button');
if (subscribeButton) {
// remove class so we don't bind twice
subscribeButton.classList.remove('js-subscribe-button');
subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
}
}
toggleSubscription(event) {
const button = event.currentTarget;
const buttonSpan = button.querySelector('span');
if (!buttonSpan || button.classList.contains('disabled')) {
return;
}
button.classList.add('disabled');
const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
const toggleActionUrl = this.containerElm.dataset.url;
$.post(toggleActionUrl, () => {
button.classList.remove('disabled');
// hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) {
gl.issueBoards.boardStoreIssueSet(
'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed,
);
} else {
buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
}
});
}
static bindAll(selector) {
[].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
}
}
window.gl = window.gl || {};
window.gl.Subscription = Subscription;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
const fieldName = $(element).data('field-name');
class SubscriptionSelect {
constructor() {
$('.js-subscription-event').each(function(i, el) {
var fieldName;
fieldName = $(el).data("field-name");
return $(el).glDropdown({
selectable: true,
fieldName: fieldName,
toggleLabel: (function(_this) {
return function(selected, el, instance) {
var $item, label;
label = 'Subscription';
$item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
};
})(this),
clicked: function(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
return $(element).glDropdown({
selectable: true,
fieldName,
toggleLabel(selected, el, instance) {
let label = 'Subscription';
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
});
return label;
},
clicked(options) {
return options.e.preventDefault();
},
id(obj, el) {
return $(el).data('id');
},
});
}
});
}
window.SubscriptionSelect = SubscriptionSelect;
......@@ -6,10 +6,9 @@
Sample configuration:
<icon
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
name="retry"
:size="32"
css-classes="top"
/>
*/
......
......@@ -25,6 +25,11 @@
type: String,
required: false,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -129,6 +134,7 @@
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/>
</div>
</div>
......
......@@ -50,7 +50,9 @@
<template>
<div class="md-header">
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }">
<li
class="md-header-tab"
:class="{ active: !previewMarkdown }">
<a
class="js-write-link"
href="#md-write-holder"
......@@ -59,7 +61,9 @@
Write
</a>
</li>
<li :class="{ active: previewMarkdown }">
<li
class="md-header-tab"
:class="{ active: previewMarkdown }">
<a
class="js-preview-link"
href="#md-preview-holder"
......@@ -68,56 +72,52 @@
Preview
</a>
</li>
<li class="pull-right">
<div class="toolbar-group">
<toolbar-button
tag="**"
button-title="Add bold text"
icon="bold" />
<toolbar-button
tag="*"
button-title="Add italic text"
icon="italic" />
<toolbar-button
tag="> "
:prepend="true"
button-title="Insert a quote"
icon="quote" />
<toolbar-button
tag="`"
tag-block="```"
button-title="Insert code"
icon="code" />
<toolbar-button
tag="* "
:prepend="true"
button-title="Add a bullet list"
icon="list-bulleted" />
<toolbar-button
tag="1. "
:prepend="true"
button-title="Add a numbered list"
icon="list-numbered" />
<toolbar-button
tag="* [ ] "
:prepend="true"
button-title="Add a task list"
icon="task-done" />
</div>
<div class="toolbar-group">
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button">
<icon
name="screen-full">
</icon>
</button>
</div>
<li class="md-header-toolbar">
<toolbar-button
tag="**"
button-title="Add bold text"
icon="bold" />
<toolbar-button
tag="*"
button-title="Add italic text"
icon="italic" />
<toolbar-button
tag="> "
:prepend="true"
button-title="Insert a quote"
icon="quote" />
<toolbar-button
tag="`"
tag-block="```"
button-title="Insert code"
icon="code" />
<toolbar-button
tag="* "
:prepend="true"
button-title="Add a bullet list"
icon="list-bulleted" />
<toolbar-button
tag="1. "
:prepend="true"
button-title="Add a numbered list"
icon="list-numbered" />
<toolbar-button
tag="* [ ] "
:prepend="true"
button-title="Add a task list"
icon="task-done" />
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button">
<icon
name="screen-full">
</icon>
</button>
</li>
</ul>
</div>
......
......@@ -9,6 +9,11 @@
type: String,
required: false,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
......@@ -41,7 +46,10 @@
are supported
</template>
</div>
<span class="uploading-container">
<span
v-if="canAttachFile"
class="uploading-container"
>
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
......
......@@ -40,7 +40,7 @@
<button
v-tooltip
type="button"
class="toolbar-btn js-md hidden-xs"
class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
:data-md-tag="tag"
......
......@@ -56,6 +56,12 @@
}
}
.blank-state-center {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
}
.blank-state {
padding: 20px;
border: 1px solid $border-color;
......@@ -66,7 +72,10 @@
align-items: center;
padding: 50px 30px;
}
}
.blank-state,
.blank-state-center {
.blank-state-icon {
svg {
display: block;
......
......@@ -2,7 +2,9 @@
.cgray { color: $common-gray; }
.clgray { color: $common-gray-light; }
.cred { color: $common-red; }
svg.cred { fill: $common-red; }
.cgreen { color: $common-green; }
svg.cgreen { fill: $common-green; }
.cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
......@@ -428,6 +430,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; }
......
......@@ -40,12 +40,6 @@
a:hover {
background-color: $link-hover-background;
color: $gl-text-color;
.settings-avatar {
svg {
fill: $gl-text-color;
}
}
}
.avatar-container {
......@@ -138,10 +132,6 @@
color: $gl-text-color-secondary;
}
svg {
fill: $gl-text-color-secondary;
}
.nav-item-name {
flex: 1;
}
......@@ -224,10 +214,6 @@
&:hover {
color: $gl-text-color;
svg {
fill: $gl-text-color;
}
}
}
......@@ -338,7 +324,6 @@
align-items: center;
svg {
fill: $gl-text-color-secondary;
margin-right: 8px;
}
......@@ -349,10 +334,6 @@
&:hover {
background-color: $border-color;
color: $gl-text-color;
svg {
fill: $gl-text-color;
}
}
}
......
......@@ -305,16 +305,11 @@
color: $gl-text-color;
border-color: $dropdown-input-focus-border;
outline: none;
svg {
fill: $gl-text-color;
}
}
svg {
height: 14px;
width: 14px;
fill: $gl-text-color-secondary;
vertical-align: middle;
}
......
......@@ -30,10 +30,6 @@
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
svg {
fill: currentColor;
}
}
&.line-separator {
......@@ -51,10 +47,6 @@
color: $color-200;
> a {
svg {
fill: $color-200;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
......
......@@ -235,10 +235,6 @@
opacity: 1;
color: $white-light;
svg {
fill: currentColor;
}
&.header-user-dropdown-toggle .header-user-avatar {
border-color: $white-light;
}
......@@ -269,14 +265,6 @@
font-size: 20px;
}
}
&.active > a,
&.dropdown.open > a {
svg {
fill: currentColor;
}
}
}
}
}
......@@ -289,10 +277,6 @@
text-decoration: none;
outline: 0;
color: $white-light;
svg {
fill: currentColor;
}
}
> a {
......@@ -307,10 +291,6 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
svg {
fill: currentColor;
}
}
&.line-separator {
......
.ci-status-icon-success,
.ci-status-icon-passed {
color: $green-500;
svg {
fill: $green-500;
}
}
.ci-status-icon-failed {
color: $gl-danger;
svg {
fill: $gl-danger;
}
}
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
color: $orange-500;
svg {
fill: $orange-500;
}
}
.ci-status-icon-running {
color: $blue-400;
svg {
fill: $blue-400;
}
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
color: $gl-text-color;
svg {
fill: $gl-text-color;
}
}
.ci-status-icon-created,
.ci-status-icon-skipped {
color: $gray-darkest;
svg {
fill: $gray-darkest;
}
}
.ci-status-icon-manual {
color: $gl-text-color;
svg {
fill: $gl-text-color;
}
}
.icon-link {
......
......@@ -27,6 +27,8 @@
}
svg {
fill: currentColor;
&.s8 { @include svg-size(8px); }
&.s12 { @include svg-size(12px); }
&.s16 { @include svg-size(16px); }
......
......@@ -57,6 +57,7 @@
.md-header {
.nav-links {
a {
width: 100%;
padding-top: 0;
line-height: 19px;
......@@ -72,6 +73,28 @@
}
}
.md-header-tab {
@media(max-width: $screen-xs-max) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
text-align: center;
}
}
.md-header-toolbar {
margin-left: auto;
@media(max-width: $screen-xs-max) {
flex: none;
display: flex;
justify-content: center;
width: 100%;
padding-top: $gl-padding-top;
padding-bottom: $gl-padding-top;
}
}
.referenced-users {
color: $gl-text-color;
padding-top: 10px;
......@@ -126,16 +149,6 @@
}
}
.toolbar-group {
float: left;
margin-right: -5px;
margin-left: $gl-padding;
&:first-child {
margin-left: 0;
}
}
.toolbar-btn {
float: left;
padding: 0 7px;
......@@ -158,6 +171,16 @@
}
}
.toolbar-fullscreen-btn {
margin-left: $gl-padding;
margin-right: -5px;
@media(max-width: $screen-xs-max) {
margin-left: 0;
margin-right: 0;
}
}
.atwho-view {
overflow-y: auto;
overflow-x: hidden;
......
......@@ -130,14 +130,6 @@
background-color: $color-light;
color: $color-dark;
border-color: $color-dark;
svg {
fill: $color-dark;
}
}
svg {
fill: $color-main;
}
}
......
......@@ -57,15 +57,7 @@
padding: 5px;
font-size: 36px;
svg {
fill: $gl-text-color;
}
&:hover {
color: $black;
svg {
fill: $black;
}
}
}
......@@ -49,6 +49,7 @@
font-size: 12px;
border-radius: 0;
border: 0;
padding: $grid-size;
.bash {
display: block;
......@@ -57,14 +58,13 @@
.top-bar {
height: 35px;
display: flex;
justify-content: flex-end;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
top: $header-height;
padding: $grid-size;
&.affix {
top: $header-height;
......@@ -90,9 +90,6 @@
}
.truncated-info {
margin: 0 auto;
align-self: center;
.truncated-info-size {
margin: 0 5px;
}
......@@ -118,7 +115,11 @@
.controllers-buttons {
color: $gl-text-color;
margin: 0 10px;
margin: 0 $grid-size;
&:last-child {
margin-right: 0;
}
}
.btn-scroll.animate {
......
......@@ -628,21 +628,46 @@
}
.diff-file-changes {
width: 450px;
max-width: 560px;
width: 100%;
z-index: 150;
@media (min-width: $screen-sm-min) {
left: $gl-padding;
}
a {
.diff-changed-file {
display: flex;
padding-top: 8px;
padding-bottom: 8px;
min-width: 0;
}
.diff-changed-file {
.diff-file-changed-icon {
margin-top: 2px;
}
.diff-changed-file-content {
display: flex;
align-items: center;
flex-direction: column;
min-width: 0;
}
.diff-changed-file-name,
.diff-changed-file-path {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.diff-changed-file-path {
direction: rtl;
color: $gl-text-color-tertiary;
}
.diff-changed-stats {
margin-left: auto;
white-space: nowrap;
}
}
......
.documentation-index {
h1 {
margin: 0;
}
h2 {
font-size: 20px;
}
li {
line-height: 24px;
color: $document-index-color;
a {
margin-right: 3px;
}
}
}
.shortcut-mappings {
font-size: 12px;
color: $help-shortcut-mapping-color;
......
......@@ -547,10 +547,6 @@ ul.notes {
width: 16px;
top: 0;
vertical-align: text-top;
path {
fill: currentColor;
}
}
.award-control-icon-positive,
......@@ -570,10 +566,6 @@ ul.notes {
.link-highlight {
color: $gl-link-color;
fill: $gl-link-color;
svg {
fill: $gl-link-color;
}
}
.award-control-icon-neutral {
......
......@@ -55,10 +55,6 @@
&:not(span):hover {
background-color: rgba($gl-text-color-secondary, .07);
}
svg {
fill: $gl-text-color-secondary;
}
}
}
......
......@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
before_action :authenticate_user_from_personal_access_token!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
......@@ -97,30 +96,15 @@ class ApplicationController < ActionController::Base
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
return try(:authenticated_user)
end
def authenticate_user_from_personal_access_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
return unless token.present?
user = User.find_by_personal_access_token(token)
# This filter handles personal access tokens, and atom requests with rss tokens
def authenticate_sessionless_user!
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
sessionless_sign_in(user)
end
# This filter handles authentication for atom request with an rss_token
def authenticate_user_from_rss_token!
return unless request.format.atom?
token = params[:rss_token].presence
return unless token.present?
user = User.find_by_rss_token(token)
sessionless_sign_in(user)
sessionless_sign_in(user) if user
end
def log_exception(exception)
......@@ -212,7 +196,11 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
return if session[:impersonator_id] || current_user&.ldap_user?
password_expires_at = current_user&.password_expires_at
if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path
end
end
......
......@@ -44,6 +44,7 @@ class AutocompleteController < ApplicationController
if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
group
end
end
......@@ -54,6 +55,7 @@ class AutocompleteController < ApplicationController
if params[:project_id].present?
project = Project.find(params[:project_id])
return render_404 unless can?(current_user, :read_project, project)
project
end
end
......
......@@ -84,6 +84,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
......@@ -4,6 +4,7 @@ class Import::GitlabProjectsController < Import::BaseController
def new
@namespace = Namespace.find(project_params[:namespace_id])
return render_404 unless current_user.can?(:create_projects, @namespace)
@path = project_params[:path]
end
......
......@@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
......@@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth
if current_user
# Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
current_user.identities
.with_extern_uid(oauth['provider'], oauth['uid'])
.first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
......
......@@ -22,12 +22,7 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
format.html do
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
end
format.html { render }
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
......@@ -112,7 +107,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
@noteable = @commit ||= @project.commit(params[:id])
@noteable = @commit ||= @project.commit_by(oid: params[:id])
end
def define_commit_vars
......
......@@ -57,6 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
@commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
end
......@@ -12,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
def metrics
return render_404 unless deployment.has_metrics?
@metrics = deployment.metrics
if @metrics&.any?
render json: @metrics, status: :ok
......
......@@ -12,6 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group
return render_404 unless can?(current_user, :read_group, group)
Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
else
flash[:alert] = 'Please select a group.'
......
......@@ -171,6 +171,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
@issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
@note = @project.notes.new(noteable: @issuable)
......
......@@ -111,6 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project),
......
......@@ -54,6 +54,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
name = request.headers['X-Gitlab-Lfs-Tmp']
return if name.include?('/')
return unless oid.present? && name.start_with?(oid)
name
end
......
......@@ -10,10 +10,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
Gitlab::GitalyClient.allow_n_plus_1_calls do
render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
def diff_for_path
......
......@@ -80,7 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def commits
# Get commits from repository
# or from cache if already merged
@commits = prepare_commits_for_rendering(@merge_request.commits)
@commits =
prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status)
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
......
......@@ -76,6 +76,7 @@ class Projects::NotesController < Projects::ApplicationController
def authorize_create_note!
return unless noteable.lockable?
access_denied! unless can?(current_user, :create_note, noteable)
end
end
......@@ -28,6 +28,7 @@ class Projects::WikisController < Projects::ApplicationController
)
else
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
......@@ -74,7 +75,11 @@ class Projects::WikisController < Projects::ApplicationController
def history
@page = @project_wiki.find_page(params[:id])
unless @page
if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
total_count: @page.count_versions)
.page(params[:page])
else
redirect_to(
project_wiki_path(@project, :home),
notice: "Page not found"
......@@ -101,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
......
......@@ -269,6 +269,7 @@ class ProjectsController < Projects::ApplicationController
def render_landing_page
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
......
......@@ -18,6 +18,7 @@ class PersonalAccessTokensFinder
def by_user(tokens)
return tokens unless @params[:user]
tokens.where(user: @params[:user])
end
......
......@@ -231,6 +231,15 @@ module ApplicationSettingsHelper
:sign_in_text,
:signup_enabled,
:terminal_max_session_time,
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_period_in_seconds,
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_requests_per_period,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_api_enabled,
:throttle_authenticated_api_requests_per_period,
:throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period,
:unique_ips_limit_enabled,
:unique_ips_limit_per_user,
......
......@@ -111,6 +111,7 @@ module DiffHelper
def diff_file_old_blob_raw_path(diff_file)
sha = diff_file.old_content_sha
return unless sha
project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
......@@ -152,11 +153,11 @@ module DiffHelper
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file?
"minus"
"file-deletion"
elsif diff_file.new_file?
"plus"
"file-addition"
else
"adjust"
"file-modified"
end
end
......
......@@ -24,6 +24,7 @@ module EmailsHelper
def action_title(url)
return unless url
%w(merge_requests issues commit).each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
......
......@@ -53,6 +53,7 @@ module MarkupHelper
# text, wrapping anything found in the requested link
fragment.children.each do |node|
next unless node.text?
node.replace(link_to(node.text, url, html_options))
end
end
......@@ -221,7 +222,7 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
class: 'toolbar-btn js-md has-tooltip hidden-xs',
class: 'toolbar-btn js-md has-tooltip',
tabindex: -1,
data: data,
title: options[:title],
......
......@@ -78,6 +78,7 @@ module NotificationsHelper
# Create hidden field to send notification setting source to controller
def hidden_setting_source_input(notification_setting)
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
......
......@@ -97,6 +97,7 @@ module TreeHelper
part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part)
yield(part, part_path)
end
end
......
......@@ -150,6 +150,7 @@ module VisibilityLevelHelper
def restricted_visibility_levels(show_all = false)
return [] if current_user.admin? && !show_all
current_application_settings.restricted_visibility_levels || []
end
......@@ -159,6 +160,7 @@ module VisibilityLevelHelper
def disallowed_visibility_level?(form_model, level)
return false unless form_model.respond_to?(:visibility_level_allowed?)
!form_model.visibility_level_allowed?(level)
end
......
......@@ -295,6 +295,15 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_requests_per_period: 3600,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_requests_per_period: 7200,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_requests_per_period: 7200,
throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48,
user_default_external: false,
polling_interval_multiplier: 1,
......
......@@ -76,12 +76,24 @@ class Blob < SimpleDelegator
new(blob, project)
end
def self.lazy(project, commit_id, path)
BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader|
project.repository.blobs_at(items.map(&:values)).each do |blob|
loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob
end
end
end
def initialize(blob, project = nil)
@project = project
super(blob)
end
def inspect
"#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>"
end
# Returns the data of the blob.
#
# If the blob is a text based blob the content is converted to UTF-8 and any
......@@ -95,7 +107,10 @@ class Blob < SimpleDelegator
end
def load_all_data!
super(project.repository) if project
# Endpoint needed: gitlab-org/gitaly#756
Gitlab::GitalyClient.allow_n_plus_1_calls do
super(project.repository) if project
end
end
def no_highlighting?
......
......@@ -317,6 +317,7 @@ module Ci
def execute_hooks
return unless project
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
......
......@@ -149,34 +149,70 @@ module Ci
end
end
# ref can't be HEAD or SHA, can only be branch/tag name
scope :latest, ->(ref = nil) do
max_id = unscope(:select)
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
if ref
where(ref: ref, id: max_id.where(ref: ref))
else
where(id: max_id)
end
end
scope :internal, -> { where(source: internal_sources) }
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
# ref - The name (or names) of the branch(es)/tag(s) to limit the list of
# pipelines to.
def self.newest_first(ref = nil)
relation = order(id: :desc)
ref ? relation.where(ref: ref) : relation
end
def self.latest_status(ref = nil)
latest(ref).status
newest_first(ref).pluck(:status).first
end
def self.latest_successful_for(ref)
success.latest(ref).order(id: :desc).first
newest_first(ref).success.take
end
def self.latest_successful_for_refs(refs)
success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
relation = newest_first(refs).success
relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
end
end
# Returns a Hash containing the latest pipeline status for every given
# commit.
#
# The keys of this Hash are the commit SHAs, the values the statuses.
#
# commits - The list of commit SHAs to get the status for.
# ref - The ref to scope the data to (e.g. "master"). If the ref is not
# given we simply get the latest status for the commits, regardless
# of what refs their pipelines belong to.
def self.latest_status_per_commit(commits, ref = nil)
p1 = arel_table
p2 = arel_table.alias
# This LEFT JOIN will filter out all but the newest row for every
# combination of (project_id, sha) or (project_id, sha, ref) if a ref is
# given.
cond = p1[:sha].eq(p2[:sha])
.and(p1[:project_id].eq(p2[:project_id]))
.and(p1[:id].lt(p2[:id]))
cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
relation = select(:sha, :status)
.where(sha: commits)
.where(p2[:id].eq(nil))
.joins(join.join_sources)
relation = relation.where(ref: ref) if ref
relation.each_with_object({}) do |row, hash|
hash[row[:sha]] = row[:status]
end
end
def self.truncate_sha(sha)
sha[0...8]
end
......@@ -300,8 +336,10 @@ module Ci
def latest?
return false unless ref
commit = project.commit(ref)
return false unless commit
commit.sha == sha
end
......@@ -469,7 +507,10 @@ module Ci
end
def latest_builds_with_artifacts
@latest_builds_with_artifacts ||= builds.latest.with_artifacts
# We purposely cast the builds to an Array here. Because we always use the
# rows if there are more than 0 this prevents us from having to run two
# queries: one to get the count and one to get the rows.
@latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a
end
private
......
......@@ -56,6 +56,7 @@ module Clusters
before_transition any => [:creating] do |provider, transition|
operation_id = transition.args.first
raise ArgumentError.new('operation_id is required') unless operation_id.present?
provider.operation_id = operation_id
end
......
......@@ -80,10 +80,11 @@ class Commit
@raw = raw_commit
@project = project
@statuses = {}
end
def id
@raw.id
raw.id
end
def ==(other)
......@@ -236,11 +237,13 @@ class Commit
end
def status(ref = nil)
@statuses ||= {}
return @statuses[ref] if @statuses.key?(ref)
@statuses[ref] = pipelines.latest_status(ref)
@statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
end
def set_status_for_ref(ref, status)
@statuses[ref] = status
end
def signature
......@@ -358,7 +361,7 @@ class Commit
@deltas ||= raw.deltas
end
def diffs(diff_options = nil)
def diffs(diff_options = {})
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
......
# frozen_string_literal: true
# A collection of Commit instances for a specific project and Git reference.
class CommitCollection
include Enumerable
attr_reader :project, :ref, :commits
# project - The project the commits belong to.
# commits - The Commit instances to store.
# ref - The name of the ref (e.g. "master").
def initialize(project, commits, ref = nil)
@project = project
@commits = commits
@ref = ref
end
def each(&block)
commits.each(&block)
end
# Sets the pipeline status for every commit.
#
# Setting this status ahead of time removes the need for running a query for
# every commit we're displaying.
def with_pipeline_status
statuses = project.pipelines.latest_status_per_commit(map(&:id), ref)
each do |commit|
commit.set_status_for_ref(ref, statuses[commit.id])
end
self
end
def respond_to_missing?(message, inc_private = false)
commits.respond_to?(message, inc_private)
end
# rubocop:disable GitlabSecurity/PublicSend
def method_missing(message, *args, &block)
commits.public_send(message, *args, &block)
end
end
......@@ -98,6 +98,7 @@ module Awardable
def create_award_emoji(name, current_user)
return unless emoji_awardable?
award_emoji.create(name: normalize_name(name), user: current_user)
end
......
......@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base
belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true
after_destroy :cleanup_fork_network
private
def cleanup_fork_network
# Explicitly using `#count` makes sure we have the correct number if the
# relation was loaded in the fork_network.
fork_network.destroy if fork_network.fork_network_members.count == 0
end
end
class Identity < ActiveRecord::Base
include Sortable
include CaseSensitivity
belongs_to :user
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap')
where(extern_uid: extern_uid, provider: provider)
iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
end
def ldap?
provider.starts_with?('ldap')
end
def self.normalize_uid(provider, uid)
if provider.to_s.starts_with?('ldap')
Gitlab::LDAP::Person.normalize_dn(uid)
else
uid.to_s
end
end
end
......@@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
if options.key?(:sidebar_endpoints) && project
url_helper = Gitlab::Routing.url_helpers
json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
end
if options.key?(:labels)
json[:labels] = labels.as_json(
......
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.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
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