Commit 13a8ad61 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into acet-mr-diffs-autosave

parents 4e734a90 d374d39c
...@@ -29,7 +29,7 @@ eslint-report.html ...@@ -29,7 +29,7 @@ eslint-report.html
/app/assets/javascripts/locale/**/app.js /app/assets/javascripts/locale/**/app.js
/backups/* /backups/*
/config/aws.yml /config/aws.yml
/config/database.yml /config/database*.yml
/config/gitlab.yml /config/gitlab.yml
/config/gitlab_ci.yml /config/gitlab_ci.yml
/config/initializers/rack_attack.rb /config/initializers/rack_attack.rb
......
...@@ -220,18 +220,6 @@ stages: ...@@ -220,18 +220,6 @@ stages:
paths: paths:
- log/development.log - log/development.log
# Review docs base
.review-docs: &review-docs
<<: *dedicated-runner
<<: *except-qa
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
when: manual
only:
- branches
# DB migration, rollback, and seed jobs # DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
...@@ -273,20 +261,44 @@ package-and-qa: ...@@ -273,20 +261,44 @@ package-and-qa:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - //@gitlab-org/gitlab-ee
# Trigger a docs build in gitlab-docs # Review docs base
# Useful to preview the docs changes live .review-docs: &review-docs
review-docs-deploy: <<: *dedicated-runner
<<: *review-docs <<: *single-script-job
stage: build variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # 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 # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup on_stop: review-docs-cleanup
# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy-manual:
<<: *review-docs
stage: build
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
when: manual
only:
- branches
<<: *except-docs-and-qa
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy:
<<: *review-docs
stage: post-test
script: script:
- gem install gitlab --no-ri --no-rdoc - gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy - ./$SCRIPT_NAME deploy
only:
- /(^docs[\/-].*|.*-docs$)/
<<: *except-qa
# Cleanup remote environment of gitlab-docs # Cleanup remote environment of gitlab-docs
review-docs-cleanup: review-docs-cleanup:
...@@ -295,9 +307,10 @@ review-docs-cleanup: ...@@ -295,9 +307,10 @@ review-docs-cleanup:
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_NAME
action: stop action: stop
when: manual
script: script:
- gem install gitlab --no-ri --no-rdoc - gem install gitlab --no-ri --no-rdoc
- ./SCRIPT_NAME cleanup - ./$SCRIPT_NAME cleanup
## ##
# Trigger a docker image build in CNG (Cloud Native GitLab) repository # Trigger a docker image build in CNG (Cloud Native GitLab) repository
......
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.0.1 (2018-06-21)
### Security (5 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
## 11.0.0 (2018-06-22) ## 11.0.0 (2018-06-22)
### Security (3 changes) ### Security (3 changes)
...@@ -242,6 +253,17 @@ entry. ...@@ -242,6 +253,17 @@ entry.
- Workhorse to send raw diff and patch for commits. - Workhorse to send raw diff and patch for commits.
## 10.8.5 (2018-06-21)
### Security (5 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
## 10.8.4 (2018-06-06) ## 10.8.4 (2018-06-06)
- No changes. - No changes.
...@@ -460,6 +482,22 @@ entry. ...@@ -460,6 +482,22 @@ entry.
- Gitaly handles repository forks by default. - Gitaly handles repository forks by default.
## 10.7.6 (2018-06-21)
### Security (6 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
- XSS fix to use safe_params instead of params in url_for helpers.
### Other (1 change)
- Replacing gollum libraries for gitlab custom libs. !18343
## 10.7.5 (2018-05-28) ## 10.7.5 (2018-05-28)
### Security (3 changes) ### Security (3 changes)
......
...@@ -650,7 +650,7 @@ the feature you contribute through all of these steps. ...@@ -650,7 +650,7 @@ the feature you contribute through all of these steps.
1. Working and clean code that is commented where needed 1. Working and clean code that is commented where needed
1. [Unit, integration, and system tests][testing] that pass on the CI server 1. [Unit, integration, and system tests][testing] that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested 1. Performance/scalability implications have been considered, addressed, and tested
1. [Documented][doc-styleguide] in the `/doc` directory 1. [Documented][doc-guidelines] in the `/doc` directory
1. [Changelog entry added][changelog], if necessary 1. [Changelog entry added][changelog], if necessary
1. Reviewed and any concerns are addressed 1. Reviewed and any concerns are addressed
1. Merged by a project maintainer 1. Merged by a project maintainer
...@@ -687,7 +687,7 @@ merge request: ...@@ -687,7 +687,7 @@ merge request:
contributors to enhance security contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md) 1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. [Documentation styleguide][doc-styleguide] 1. [Documentation styleguide](https://docs.gitlab.com/ee/development/documentation/styleguide.html)
1. Interface text should be written subjectively instead of objectively. It 1. Interface text should be written subjectively instead of objectively. It
should be the GitLab core team addressing a person. It should be written in should be the GitLab core team addressing a person. It should be written in
present time and never use past tense (has been/was). For example instead present time and never use past tense (has been/was). For example instead
......
...@@ -230,7 +230,7 @@ gem 'ruby-fogbugz', '~> 0.2.1' ...@@ -230,7 +230,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0' gem 'kubeclient', '~> 3.1.0'
# Sanitize user input # Sanitize user input
gem 'sanitize', '~> 2.0' gem 'sanitize', '~> 4.6.5'
gem 'babosa', '~> 1.0.2' gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input # Sanitizes SVG input
......
...@@ -295,13 +295,13 @@ GEM ...@@ -295,13 +295,13 @@ GEM
flowdock (~> 0.7) flowdock (~> 0.7)
gitlab-grit (>= 2.4.1) gitlab-grit (>= 2.4.1)
multi_json multi_json
gitlab-gollum-lib (4.2.7.4) gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2) gemojione (~> 3.2)
github-markup (~> 1.6) github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0) gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0) nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1) rouge (~> 3.1)
sanitize (~> 2.1) sanitize (~> 4.6.4)
stringex (~> 2.6) stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1) gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15) mime-types (>= 1.15)
...@@ -514,6 +514,8 @@ GEM ...@@ -514,6 +514,8 @@ GEM
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.8.2) nokogiri (1.8.2)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
numerizer (0.1.1) numerizer (0.1.1)
oauth (0.5.4) oauth (0.5.4)
oauth2 (1.4.0) oauth2 (1.4.0)
...@@ -804,8 +806,10 @@ GEM ...@@ -804,8 +806,10 @@ GEM
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.2) rugged (0.27.2)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (4.6.5)
crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
sass (3.5.5) sass (3.5.5)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
...@@ -1151,7 +1155,7 @@ DEPENDENCIES ...@@ -1151,7 +1155,7 @@ DEPENDENCIES
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.27) rugged (~> 0.27)
sanitize (~> 2.0) sanitize (~> 4.6.5)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
......
...@@ -298,13 +298,13 @@ GEM ...@@ -298,13 +298,13 @@ GEM
flowdock (~> 0.7) flowdock (~> 0.7)
gitlab-grit (>= 2.4.1) gitlab-grit (>= 2.4.1)
multi_json multi_json
gitlab-gollum-lib (4.2.7.4) gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2) gemojione (~> 3.2)
github-markup (~> 1.6) github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0) gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0) nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1) rouge (~> 3.1)
sanitize (~> 2.1) sanitize (~> 4.6.4)
stringex (~> 2.6) stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1) gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15) mime-types (>= 1.15)
...@@ -518,6 +518,8 @@ GEM ...@@ -518,6 +518,8 @@ GEM
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.8.2) nokogiri (1.8.2)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
numerizer (0.1.1) numerizer (0.1.1)
oauth (0.5.4) oauth (0.5.4)
oauth2 (1.4.0) oauth2 (1.4.0)
...@@ -813,8 +815,10 @@ GEM ...@@ -813,8 +815,10 @@ GEM
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.1) rugged (0.27.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (4.6.5)
crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
sass (3.5.5) sass (3.5.5)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
...@@ -1162,7 +1166,7 @@ DEPENDENCIES ...@@ -1162,7 +1166,7 @@ DEPENDENCIES
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.27) rugged (~> 0.27)
sanitize (~> 2.0) sanitize (~> 4.6.5)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
......
...@@ -6,13 +6,13 @@ import Flash from '../../flash'; ...@@ -6,13 +6,13 @@ import Flash from '../../flash';
import { __ } from '../../locale'; import { __ } from '../../locale';
import Sidebar from '../../right_sidebar'; import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
import assignees from '../../sidebar/components/assignees/assignees.vue'; import Assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import RemoveBtn from './sidebar/remove_issue.vue';
import IssuableContext from '../../issuable_context'; import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select'; import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select'; import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -22,10 +22,10 @@ window.gl.issueBoards = window.gl.issueBoards || {}; ...@@ -22,10 +22,10 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({ gl.issueBoards.BoardSidebar = Vue.extend({
components: { components: {
assigneeTitle, AssigneeTitle,
assignees, Assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn, RemoveBtn,
subscriptions, Subscriptions,
}, },
props: { props: {
currentUser: { currentUser: {
......
import Vue from 'vue'; <script>
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import './lists_dropdown'; import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility'; import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalFooter = Vue.extend({ export default {
components: { components: {
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, ListsDropdown,
}, },
mixins: [modalMixin], mixins: [modalMixin],
data() { data() {
...@@ -55,28 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -55,28 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({
this.toggleModal(false); this.toggleModal(false);
}, },
}, },
template: ` };
<footer </script>
class="form-actions add-issues-footer"> <template>
<div class="float-left"> <footer
<button class="form-actions add-issues-footer"
class="btn btn-success" >
type="button" <div class="float-left">
:disabled="submitDisabled"
@click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown></lists-dropdown>
</div>
<button <button
class="btn btn-default float-right" :disabled="submitDisabled"
class="btn btn-success"
type="button" type="button"
@click="toggleModal(false)"> @click="addIssues"
Cancel >
{{ submitText }}
</button> </button>
</footer> <span class="inline add-issues-footer-to-list">
`, to list
}); </span>
<lists-dropdown/>
</div>
<button
class="btn btn-default float-right"
type="button"
@click="toggleModal(false)"
>
Cancel
</button>
</footer>
</template>
import Vue from 'vue'; import Vue from 'vue';
import modalFilters from './filters'; import modalFilters from './filters';
import './tabs'; import modalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({ gl.issueBoards.ModalHeader = Vue.extend({
components: { components: {
'modal-tabs': gl.issueBoards.ModalTabs, modalTabs,
modalFilters, modalFilters,
}, },
mixins: [modalMixin], mixins: [modalMixin],
......
...@@ -5,7 +5,7 @@ import queryData from '~/boards/utils/query_data'; ...@@ -5,7 +5,7 @@ import queryData from '~/boards/utils/query_data';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header'; import './header';
import './list'; import './list';
import './footer'; import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
...@@ -14,7 +14,7 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -14,7 +14,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
EmptyState, EmptyState,
'modal-header': gl.issueBoards.ModalHeader, 'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList, 'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter, ModalFooter,
loadingIcon, loadingIcon,
}, },
props: { props: {
......
import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
this.modal.selectedList = null;
},
template: `
<div class="dropdown inline">
<button
class="dropdown-menu-toggle"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<span
class="dropdown-label-box"
:style="{ backgroundColor: selected.label.color }">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="list in state.lists"
v-if="list.type == 'label'">
<a
href="#"
role="button"
:class="{ 'is-active': list.id == selected.id }"
@click.prevent="modal.selectedList = list">
<span
class="dropdown-label-box"
:style="{ backgroundColor: list.label.color }">
</span>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div>
`,
});
<script>
import ModalStore from '../../stores/modal_store';
export default {
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
this.modal.selectedList = null;
},
};
</script>
<template>
<div class="dropdown inline">
<button
class="dropdown-menu-toggle"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<span
:style="{ backgroundColor: selected.label.color }"
class="dropdown-label-box">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="(list, i) in state.lists"
v-if="list.type == 'label'"
:key="i">
<a
:class="{ 'is-active': list.id == selected.id }"
href="#"
role="button"
@click.prevent="modal.selectedList = list">
<span
:style="{ backgroundColor: list.label.color }"
class="dropdown-label-box">
</span>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div>
</template>
import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalTabs = Vue.extend({
mixins: [modalMixin],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
template: `
<div class="top-area prepend-top-10 append-bottom-10">
<ul class="nav-links issues-state-filters">
<li :class="{ 'active': activeTab == 'all' }">
<a
href="#"
role="button"
@click.prevent="changeTab('all')">
Open issues
<span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
</li>
<li :class="{ 'active': activeTab == 'selected' }">
<a
href="#"
role="button"
@click.prevent="changeTab('selected')">
Selected issues
<span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
</li>
</ul>
</div>
`,
});
<script>
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
mixins: [modalMixin],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
};
</script>
<template>
<div class="top-area prepend-top-10 append-bottom-10">
<ul class="nav-links issues-state-filters">
<li :class="{ 'active': activeTab == 'all' }">
<a
href="#"
role="button"
@click.prevent="changeTab('all')"
>
Open issues
<span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
</li>
<li :class="{ 'active': activeTab == 'selected' }">
<a
href="#"
role="button"
@click.prevent="changeTab('selected')"
>
Selected issues
<span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
</li>
</ul>
</div>
</template>
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.RemoveIssueBtn = Vue.extend({
props: {
issue: {
type: Object,
required: true,
},
list: {
type: Object,
required: true,
},
},
computed: {
updateUrl() {
return this.issue.path;
},
},
methods: {
removeIssue() {
const issue = this.issue;
const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id);
let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const data = {
issue: {
label_ids: labelIds,
},
};
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach((list) => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach((list) => {
list.removeIssue(issue);
});
Store.detail.issue = {};
},
},
template: `
<div
class="block list">
<button
class="btn btn-default btn-block"
type="button"
@click="removeIssue">
Remove from board
</button>
</div>
`,
});
<script>
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore;
export default {
props: {
issue: {
type: Object,
required: true,
},
list: {
type: Object,
required: true,
},
},
computed: {
updateUrl() {
return this.issue.path;
},
},
methods: {
removeIssue() {
const issue = this.issue;
const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id);
let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const data = {
issue: {
label_ids: labelIds,
},
};
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach(list => {
list.removeIssue(issue);
});
Store.detail.issue = {};
},
},
};
</script>
<template>
<div
class="block list"
>
<button
class="btn btn-default btn-block"
type="button"
@click="removeIssue"
>
Remove from board
</button>
</div>
</template>
...@@ -7,6 +7,16 @@ function sanitize(str) { ...@@ -7,6 +7,16 @@ function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, ''); return str.replace(/<(?:.|\n)*?>/gm, '');
} }
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
issues: true,
mergeRequests: true,
epics: false,
milestones: true,
labels: true,
};
class GfmAutoComplete { class GfmAutoComplete {
constructor(dataSources) { constructor(dataSources) {
this.dataSources = dataSources || {}; this.dataSources = dataSources || {};
...@@ -14,14 +24,7 @@ class GfmAutoComplete { ...@@ -14,14 +24,7 @@ class GfmAutoComplete {
this.isLoadingData = {}; this.isLoadingData = {};
} }
setup(input, enableMap = { setup(input, enableMap = defaultAutocompleteConfig) {
emojis: true,
members: true,
issues: true,
milestones: true,
mergeRequests: true,
labels: true,
}) {
// Add GFM auto-completion to all input fields, that accept GFM input. // Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input'); this.input = input || $('.js-gfm-input');
this.enableMap = enableMap; this.enableMap = enableMap;
......
import $ from 'jquery'; import $ from 'jquery';
import autosize from 'autosize'; import autosize from 'autosize';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete';
import dropzoneInput from './dropzone_input'; import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm { export default class GLForm {
constructor(form, enableGFM = false) { constructor(form, enableGFM = {}) {
this.form = form; this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input'); this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = enableGFM; this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
// Before we start, we should clean up any previous data for this form // Before we start, we should clean up any previous data for this form
this.destroy(); this.destroy();
// Setup the form // Setup the form
...@@ -34,14 +34,7 @@ export default class GLForm { ...@@ -34,14 +34,7 @@ export default class GLForm {
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), { this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
emojis: true,
members: this.enableGFM,
issues: this.enableGFM,
milestones: this.enableGFM,
mergeRequests: this.enableGFM,
labels: this.enableGFM,
});
dropzoneInput(this.form); dropzoneInput(this.form);
autosize(this.textarea); autosize(this.textarea);
} }
......
...@@ -89,14 +89,14 @@ export default { ...@@ -89,14 +89,14 @@ export default {
<template> <template>
<div class="multi-file-commit-list-item position-relative"> <div class="multi-file-commit-list-item position-relative">
<button <div
v-tooltip v-tooltip
:title="tooltipTitle" :title="tooltipTitle"
:class="{ :class="{
'is-active': isActive 'is-active': isActive
}" }"
type="button"
class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0" class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
role="button"
@dblclick="fileAction" @dblclick="fileAction"
@click="openFileInEditor" @click="openFileInEditor"
> >
...@@ -107,7 +107,7 @@ export default { ...@@ -107,7 +107,7 @@ export default {
:css-classes="iconClass" :css-classes="iconClass"
/>{{ file.name }} />{{ file.name }}
</span> </span>
</button> </div>
<component <component
:is="actionComponent" :is="actionComponent"
:path="file.path" :path="file.path"
......
...@@ -69,7 +69,7 @@ export default { ...@@ -69,7 +69,7 @@ export default {
> >
<icon <icon
:size="16" :size="16"
name="pipeline" name="rocket"
/> />
</button> </button>
</li> </li>
......
...@@ -44,6 +44,8 @@ export default { ...@@ -44,6 +44,8 @@ export default {
methods: { methods: {
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']), ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
clickFile(tab) { clickFile(tab) {
if (tab.active) return;
this.updateDelayViewerUpdated(true); this.updateDelayViewerUpdated(true);
if (tab.pending) { if (tab.pending) {
......
...@@ -7,10 +7,10 @@ export default () => { ...@@ -7,10 +7,10 @@ export default () => {
notesIds, notesIds,
now, now,
diffView, diffView,
autocomplete, enableGFM,
} = JSON.parse(dataEl.innerHTML); } = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign // Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance // into the window object, we can just access the current isntance with Notes.instance
Notes.initialize(notesUrl, notesIds, now, diffView, autocomplete); Notes.initialize(notesUrl, notesIds, now, diffView, enableGFM);
}; };
...@@ -20,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c ...@@ -20,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash'; import Flash from './flash';
import { defaultAutocompleteConfig } from './gfm_auto_complete';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form'; import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
...@@ -45,7 +46,7 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; ...@@ -45,7 +46,7 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes { export default class Notes {
static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM) {
if (!this.instance) { if (!this.instance) {
this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM); this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
} }
...@@ -55,7 +56,7 @@ export default class Notes { ...@@ -55,7 +56,7 @@ export default class Notes {
return this.instance; return this.instance;
} }
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
this.updateTargetButtons = this.updateTargetButtons.bind(this); this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this); this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this); this.visibilityChange = this.visibilityChange.bind(this);
...@@ -94,7 +95,7 @@ export default class Notes { ...@@ -94,7 +95,7 @@ export default class Notes {
this.cleanBinding(); this.cleanBinding();
this.addBinding(); this.addBinding();
this.setPollingInterval(); this.setPollingInterval();
this.setupMainTargetNoteForm(); this.setupMainTargetNoteForm(enableGFM);
this.taskList = new TaskList({ this.taskList = new TaskList({
dataType: 'note', dataType: 'note',
fieldName: 'note', fieldName: 'note',
...@@ -598,14 +599,14 @@ export default class Notes { ...@@ -598,14 +599,14 @@ export default class Notes {
* *
* Sets some hidden fields in the form. * Sets some hidden fields in the form.
*/ */
setupMainTargetNoteForm() { setupMainTargetNoteForm(enableGFM) {
var form; var form;
// find the form // find the form
form = $('.js-new-note-form'); form = $('.js-new-note-form');
// Set a global clone of the form for later cloning // Set a global clone of the form for later cloning
this.formClone = form.clone(); this.formClone = form.clone();
// show the form // show the form
this.setupNoteForm(form); this.setupNoteForm(form, enableGFM);
// fix classes // fix classes
form.removeClass('js-new-note-form'); form.removeClass('js-new-note-form');
form.addClass('js-main-target-form'); form.addClass('js-main-target-form');
...@@ -633,9 +634,9 @@ export default class Notes { ...@@ -633,9 +634,9 @@ export default class Notes {
* setup GFM auto complete * setup GFM auto complete
* show the form * show the form
*/ */
setupNoteForm(form) { setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
var textarea, key; var textarea, key;
this.glForm = new GLForm(form, this.enableGFM); this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text'); textarea = form.find('.js-note-text');
key = [ key = [
'Note', 'Note',
......
...@@ -194,7 +194,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" ...@@ -194,7 +194,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button" type="button"
@click="cancelHandler()"> @click="cancelHandler()">
Cancel {{ __('Discard draft') }}
</button> </button>
</div> </div>
</form> </form>
......
...@@ -3,5 +3,5 @@ import GLForm from '~/gl_form'; ...@@ -3,5 +3,5 @@ import GLForm from '~/gl_form';
export default function ($formEl) { export default function ($formEl) {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($formEl, true); // eslint-disable-line no-new new GLForm($formEl); // eslint-disable-line no-new
} }
...@@ -10,7 +10,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; ...@@ -10,7 +10,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => { export default () => {
new ShortcutsNavigation(); new ShortcutsNavigation();
new GLForm($('.issue-form'), true); new GLForm($('.issue-form'));
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
......
...@@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; ...@@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => { export default () => {
new Diff(); new Diff();
new ShortcutsNavigation(); new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true); new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
......
...@@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form'; ...@@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($('.tag-form'), true); // eslint-disable-line no-new new GLForm($('.tag-form')); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
}); });
...@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new new GLForm($('.wiki-form')); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button'); const deleteWikiButton = document.getElementById('delete-wiki-button');
......
...@@ -3,6 +3,13 @@ import GLForm from '~/gl_form'; ...@@ -3,6 +3,13 @@ import GLForm from '~/gl_form';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
export default () => { export default () => {
new GLForm($('.snippet-form'), false); // eslint-disable-line no-new // eslint-disable-next-line no-new
new GLForm($('.snippet-form'), {
members: false,
issues: false,
mergeRequests: false,
milestones: false,
labels: false,
});
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
}; };
...@@ -6,5 +6,13 @@ import GLForm from '../../gl_form'; ...@@ -6,5 +6,13 @@ import GLForm from '../../gl_form';
export default (initGFM = true) => { export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new DueDateSelectors(); // eslint-disable-line no-new new DueDateSelectors(); // eslint-disable-line no-new
new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new // eslint-disable-next-line no-new
new GLForm($('.milestone-form'), {
emojis: initGFM,
members: initGFM,
issues: initGFM,
mergeRequests: initGFM,
milestones: initGFM,
labels: initGFM,
});
}; };
...@@ -11,7 +11,6 @@ export default class U2FAuthenticate { ...@@ -11,7 +11,6 @@ export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) { constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.u2fUtils = null; this.u2fUtils = null;
this.container = container; this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this); this.renderAuthenticated = this.renderAuthenticated.bind(this);
this.renderError = this.renderError.bind(this); this.renderError = this.renderError.bind(this);
this.renderInProgress = this.renderInProgress.bind(this); this.renderInProgress = this.renderInProgress.bind(this);
...@@ -41,7 +40,6 @@ export default class U2FAuthenticate { ...@@ -41,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge')); this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
this.templates = { this.templates = {
notSupported: '#js-authenticate-u2f-not-supported',
setup: '#js-authenticate-u2f-setup', setup: '#js-authenticate-u2f-setup',
inProgress: '#js-authenticate-u2f-in-progress', inProgress: '#js-authenticate-u2f-in-progress',
error: '#js-authenticate-u2f-error', error: '#js-authenticate-u2f-error',
...@@ -55,7 +53,7 @@ export default class U2FAuthenticate { ...@@ -55,7 +53,7 @@ export default class U2FAuthenticate {
this.u2fUtils = utils; this.u2fUtils = utils;
this.renderInProgress(); this.renderInProgress();
}) })
.catch(() => this.renderNotSupported()); .catch(() => this.switchToFallbackUI());
} }
authenticate() { authenticate() {
...@@ -96,10 +94,6 @@ export default class U2FAuthenticate { ...@@ -96,10 +94,6 @@ export default class U2FAuthenticate {
this.fallbackButton.classList.add('hidden'); this.fallbackButton.classList.add('hidden');
} }
renderNotSupported() {
return this.renderTemplate('notSupported');
}
switchToFallbackUI() { switchToFallbackUI() {
this.fallbackButton.classList.add('hidden'); this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden'); this.container[0].classList.add('hidden');
......
...@@ -62,7 +62,14 @@ ...@@ -62,7 +62,14 @@
/* /*
GLForm class handles all the toolbar buttons GLForm class handles all the toolbar buttons
*/ */
return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
});
}, },
beforeDestroy() { beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm'); const glForm = $(this.$refs['gl-form']).data('glForm');
......
...@@ -310,7 +310,7 @@ pre code { ...@@ -310,7 +310,7 @@ pre code {
color: $white-light; color: $white-light;
h4, h4,
a, a:not(.btn),
.alert-link { .alert-link {
color: $white-light; color: $white-light;
} }
......
...@@ -180,10 +180,6 @@ ...@@ -180,10 +180,6 @@
color: $border-and-box-shadow; color: $border-and-box-shadow;
} }
.ide-file-list .file.file-active {
color: $border-and-box-shadow;
}
.ide-sidebar-link { .ide-sidebar-link {
&.active { &.active {
color: $border-and-box-shadow; color: $border-and-box-shadow;
......
...@@ -527,7 +527,7 @@ ...@@ -527,7 +527,7 @@
.header-user { .header-user {
.dropdown-menu { .dropdown-menu {
width: auto; width: auto;
min-width: 160px; min-width: unset;
margin-top: 4px; margin-top: 4px;
color: $gl-text-color; color: $gl-text-color;
left: auto; left: auto;
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
margin-top: 0; margin-top: 0;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height; padding-bottom: $ide-statusbar-height;
color: $gl-text-color;
&.is-collapsed { &.is-collapsed {
.ide-file-list { .ide-file-list {
...@@ -45,12 +46,8 @@ ...@@ -45,12 +46,8 @@
.file { .file {
cursor: pointer; cursor: pointer;
&.file-open {
background: $white-normal;
}
&.file-active { &.file-active {
font-weight: $gl-font-weight-bold; background: $theme-gray-100;
} }
.ide-file-name { .ide-file-name {
...@@ -58,7 +55,9 @@ ...@@ -58,7 +55,9 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: inherit; max-width: inherit;
line-height: 22px; line-height: 16px;
display: inline-block;
height: 18px;
svg { svg {
vertical-align: middle; vertical-align: middle;
...@@ -86,12 +85,14 @@ ...@@ -86,12 +85,14 @@
.ide-new-btn { .ide-new-btn {
display: none; display: none;
.btn {
padding: 2px 5px;
}
} }
&:hover, &:hover,
&:focus { &:focus {
background: $white-normal;
.ide-new-btn { .ide-new-btn {
display: block; display: block;
} }
...@@ -281,8 +282,8 @@ ...@@ -281,8 +282,8 @@
} }
.margin { .margin {
background-color: $gray-light; background-color: $white-light;
border-right: 1px solid $white-normal; border-right: 1px solid $theme-gray-100;
.line-insert { .line-insert {
border-right: 1px solid $line-added-dark; border-right: 1px solid $line-added-dark;
...@@ -303,6 +304,15 @@ ...@@ -303,6 +304,15 @@
.multi-file-editor-holder { .multi-file-editor-holder {
height: 100%; height: 100%;
min-height: 0; min-height: 0;
&.is-readonly,
.editor.original {
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
background-color: $theme-gray-50;
}
}
} }
.preview-container { .preview-container {
...@@ -587,11 +597,17 @@ ...@@ -587,11 +597,17 @@
&:hover, &:hover,
&:focus { &:focus {
background: $white-normal; background: $theme-gray-100;
}
&:active {
background: $theme-gray-200;
} }
} }
.multi-file-commit-list-path { .multi-file-commit-list-path {
cursor: pointer;
&.is-active { &.is-active {
background-color: $white-normal; background-color: $white-normal;
} }
...@@ -611,10 +627,6 @@ ...@@ -611,10 +627,6 @@
.multi-file-commit-list-file-path { .multi-file-commit-list-file-path {
@include str-truncated(calc(100% - 30px)); @include str-truncated(calc(100% - 30px));
&:hover {
text-decoration: underline;
}
&:active { &:active {
text-decoration: none; text-decoration: none;
} }
......
...@@ -114,7 +114,7 @@ input[type="checkbox"]:hover { ...@@ -114,7 +114,7 @@ input[type="checkbox"]:hover {
} }
.dropdown-content { .dropdown-content {
max-height: 302px; max-height: none;
} }
} }
......
...@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private private
def set_application_setting def set_application_setting
@application_setting = ApplicationSetting.current_without_cache @application_setting = Gitlab::CurrentSettings.current_application_settings
end end
def application_setting_params def application_setting_params
......
...@@ -72,10 +72,10 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -72,10 +72,10 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(group_params_ce) params.require(:group).permit(allowed_group_params)
end end
def group_params_ce def allowed_group_params
[ [
:avatar, :avatar,
:description, :description,
......
...@@ -187,10 +187,10 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -187,10 +187,10 @@ class Admin::UsersController < Admin::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(user_params_ce) params.require(:user).permit(allowed_user_params)
end end
def user_params_ce def allowed_user_params
[ [
:access_level, :access_level,
:avatar, :avatar,
......
...@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
set_remember_me(user) set_remember_me(user)
if user.two_factor_enabled? if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user) prompt_for_two_factor(user)
else else
sign_in_and_redirect(user) sign_in_and_redirect(user)
......
...@@ -160,7 +160,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -160,7 +160,7 @@ class Projects::JobsController < Projects::ApplicationController
def build def build
@build ||= project.builds.find(params[:id]) @build ||= project.builds.find(params[:id])
.present(current_user: current_user) .present(current_user: current_user)
end end
def build_path(build) def build_path(build)
......
...@@ -95,6 +95,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -95,6 +95,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy def destroy
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
WikiPages::DestroyService.new(@project, current_user).execute(@page) WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to project_wiki_path(@project, :home), redirect_to project_wiki_path(@project, :home),
......
...@@ -63,7 +63,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -63,7 +63,7 @@ class ProjectsController < Projects::ApplicationController
redirect_to(edit_project_path(@project)) redirect_to(edit_project_path(@project))
end end
else else
flash[:alert] = result[:message] flash.now[:alert] = result[:message]
format.html { render 'edit' } format.html { render 'edit' }
end end
......
...@@ -56,7 +56,7 @@ class UserRecentEventsFinder ...@@ -56,7 +56,7 @@ class UserRecentEventsFinder
visible = target_user visible = target_user
.project_interactions .project_interactions
.where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]) .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
.select(:id) .select(:id)
Gitlab::SQL::Union.new([authorized, visible]).to_sql Gitlab::SQL::Union.new([authorized, visible]).to_sql
......
...@@ -143,7 +143,14 @@ module NotesHelper ...@@ -143,7 +143,14 @@ module NotesHelper
notesIds: @notes.map(&:id), notesIds: @notes.map(&:id),
now: Time.now.to_i, now: Time.now.to_i,
diffView: diff_view, diffView: diff_view,
autocomplete: autocomplete enableGFM: {
emojis: true,
members: autocomplete,
issues: autocomplete,
mergeRequests: autocomplete,
milestones: autocomplete,
labels: autocomplete
}
} }
end end
......
...@@ -40,7 +40,8 @@ module ProjectsHelper ...@@ -40,7 +40,8 @@ module ProjectsHelper
name_tag_options[:class] << 'has-tooltip' name_tag_options[:class] << 'has-tooltip'
end end
content_tag(:span, sanitize(username), name_tag_options) # NOTE: ActionView::Helpers::TagHelper#content_tag HTML escapes username
content_tag(:span, username, name_tag_options)
end end
def link_to_member(project, author, opts = {}, &block) def link_to_member(project, author, opts = {}, &block)
...@@ -506,6 +507,14 @@ module ProjectsHelper ...@@ -506,6 +507,14 @@ module ProjectsHelper
end end
end end
def sidebar_projects_paths
%w[
projects#show
projects#activity
cycle_analytics#show
]
end
def sidebar_settings_paths def sidebar_settings_paths
%w[ %w[
projects#edit projects#edit
......
...@@ -212,14 +212,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -212,14 +212,6 @@ class ApplicationSetting < ActiveRecord::Base
end end
end end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
value&.each do |source|
unless Devise.omniauth_providers.include?(source.to_sym)
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
end
end
validate :terms_exist, if: :enforce_terms? validate :terms_exist, if: :enforce_terms?
before_validation :ensure_uuid! before_validation :ensure_uuid!
...@@ -330,6 +322,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -330,6 +322,11 @@ class ApplicationSetting < ActiveRecord::Base
::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled) ::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
end end
def disabled_oauth_sign_in_sources=(sources)
sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s)
super(sources)
end
def domain_whitelist_raw def domain_whitelist_raw
self.domain_whitelist&.join("\n") self.domain_whitelist&.join("\n")
end end
......
...@@ -561,9 +561,9 @@ module Ci ...@@ -561,9 +561,9 @@ module Ci
.append(key: 'CI_PIPELINE_IID', value: iid.to_s) .append(key: 'CI_PIPELINE_IID', value: iid.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message) .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title) .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description) .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
end end
def queued_duration def queued_duration
......
...@@ -12,8 +12,8 @@ module Sortable ...@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder("lower(name) asc") } scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
scope :order_name_desc, -> { reorder("lower(name) desc") } scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end end
module ClassMethods module ClassMethods
......
...@@ -129,9 +129,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -129,9 +129,7 @@ class MergeRequest < ActiveRecord::Base
after_transition unchecked: :cannot_be_merged do |merge_request, transition| after_transition unchecked: :cannot_be_merged do |merge_request, transition|
begin begin
# Merge request can become unmergeable due to many reasons. if merge_request.notify_conflict?
# We only notify if it is due to conflict.
unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
NotificationService.new.merge_request_unmergeable(merge_request) NotificationService.new.merge_request_unmergeable(merge_request)
TodoService.new.merge_request_became_unmergeable(merge_request) TodoService.new.merge_request_became_unmergeable(merge_request)
end end
...@@ -378,6 +376,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -378,6 +376,10 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def non_latest_diffs
merge_request_diffs.where.not(id: merge_request_diff.id)
end
def diff_size def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform # Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here. # highlighting, which we don't need here.
...@@ -619,18 +621,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -619,18 +621,7 @@ class MergeRequest < ActiveRecord::Base
def reload_diff(current_user = nil) def reload_diff(current_user = nil)
return unless open? return unless open?
old_diff_refs = self.diff_refs MergeRequests::ReloadDiffsService.new(self, current_user).execute
new_diff = create_merge_request_diff
MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
new_diff_refs = self.diff_refs
update_diff_discussion_positions(
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
current_user: current_user
)
end end
def check_if_can_be_merged def check_if_can_be_merged
...@@ -715,6 +706,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -715,6 +706,10 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch? should_remove_source_branch? || force_remove_source_branch?
end end
def notify_conflict?
(opened? || locked?) && !project.repository.can_be_merged?(diff_head_sha, target_branch)
end
def related_notes def related_notes
# Fetch comments only from last 100 commits # Fetch comments only from last 100 commits
commits_for_notes_limit = 100 commits_for_notes_limit = 100
......
...@@ -3,6 +3,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -3,6 +3,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Importable include Importable
include ManualInverseAssociation include ManualInverseAssociation
include IgnorableColumn include IgnorableColumn
include EachBatch
# Don't display more than 100 commits at once # Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100 COMMITS_SAFE_SIZE = 100
...@@ -17,8 +18,14 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -17,8 +18,14 @@ class MergeRequestDiff < ActiveRecord::Base
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) } has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
event :clean do
transition any => :without_files
end
state :collected state :collected
state :overflow state :overflow
# Diff files have been deleted by the system
state :without_files
# Deprecated states: these are no longer used but these values may still occur # Deprecated states: these are no longer used but these values may still occur
# in the database. # in the database.
state :timeout state :timeout
...@@ -27,6 +34,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -27,6 +34,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit state :overflow_diff_lines_limit
end end
scope :with_files, -> { without_states(:without_files, :empty) }
scope :viewable, -> { without_state(:empty) } scope :viewable, -> { without_state(:empty) }
scope :by_commit_sha, ->(sha) do scope :by_commit_sha, ->(sha) do
joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil) joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
...@@ -42,6 +50,10 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -42,6 +50,10 @@ class MergeRequestDiff < ActiveRecord::Base
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha) find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end end
def viewable?
collected? || without_files? || overflow?
end
# Collect information about commits and diff from repository # Collect information about commits and diff from repository
# and save it to the database as serialized data # and save it to the database as serialized data
def save_git_content def save_git_content
...@@ -170,6 +182,21 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -170,6 +182,21 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def diffs(diff_options = nil) def diffs(diff_options = nil)
if without_files? && comparison = diff_refs.compare_in(project)
# It should fetch the repository when diffs are cleaned by the system.
# We don't keep these for storage overload purposes.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/37639
comparison.diffs(diff_options)
else
diffs_collection(diff_options)
end
end
# Should always return the DB persisted diffs collection
# (e.g. Gitlab::Diff::FileCollection::MergeRequestDiff.
# It's useful when trying to invalidate old caches through
# FileCollection::MergeRequestDiff#clear_cache!
def diffs_collection(diff_options = nil)
Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options) Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
end end
......
...@@ -228,6 +228,10 @@ class Namespace < ActiveRecord::Base ...@@ -228,6 +228,10 @@ class Namespace < ActiveRecord::Base
parent.present? parent.present?
end end
def root_ancestor
ancestors.reorder(nil).find_by(parent_id: nil)
end
def subgroup? def subgroup?
has_parent? has_parent?
end end
......
...@@ -2019,6 +2019,10 @@ class Project < ActiveRecord::Base ...@@ -2019,6 +2019,10 @@ class Project < ActiveRecord::Base
end end
request_cache(:any_lfs_file_locks?) { self.id } request_cache(:any_lfs_file_locks?) { self.id }
def auto_cancel_pending_pipelines?
auto_cancel_pending_pipelines == 'enabled'
end
private private
def storage def storage
......
...@@ -29,8 +29,8 @@ class ProjectAutoDevops < ActiveRecord::Base ...@@ -29,8 +29,8 @@ class ProjectAutoDevops < ActiveRecord::Base
end end
if manual? if manual?
variables.append(key: 'STAGING_ENABLED', value: 1) variables.append(key: 'STAGING_ENABLED', value: '1')
variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1) variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
end end
end end
end end
......
...@@ -24,7 +24,7 @@ class ProjectTeam ...@@ -24,7 +24,7 @@ class ProjectTeam
end end
def add_role(user, role, current_user: nil) def add_role(user, role, current_user: nil)
send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end end
def find_member(user_id) def find_member(user_id)
......
...@@ -21,7 +21,7 @@ class Repository ...@@ -21,7 +21,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError) CreateTreeError = Class.new(StandardError)
......
...@@ -17,7 +17,7 @@ class BaseCountService ...@@ -17,7 +17,7 @@ class BaseCountService
end end
def refresh_cache(&block) def refresh_cache(&block)
Rails.cache.write(cache_key, block_given? ? yield : uncached_count, raw: raw?) update_cache_for_key(cache_key, &block)
end end
def uncached_count def uncached_count
...@@ -41,4 +41,8 @@ class BaseCountService ...@@ -41,4 +41,8 @@ class BaseCountService
def cache_options def cache_options
{ raw: raw? } { raw: raw? }
end end
def update_cache_for_key(key, &block)
Rails.cache.write(key, block_given? ? yield : uncached_count, raw: raw?)
end
end end
module MergeRequests
class DeleteNonLatestDiffsService
BATCH_SIZE = 10
def initialize(merge_request)
@merge_request = merge_request
end
def execute
diffs = @merge_request.non_latest_diffs.with_files
diffs.each_batch(of: BATCH_SIZE) do |relation, index|
ids = relation.pluck(:id).map { |id| [id] }
DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids)
end
end
end
end
module MergeRequests
class MergeRequestDiffCacheService
def execute(merge_request, new_diff)
# Executing the iteration we cache all the highlighted diff information
merge_request.diffs.diff_files.to_a
# Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when
# reloading the diff.
MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
next if merge_request_diff == new_diff
merge_request_diff.diffs.clear_cache!
end
end
end
end
...@@ -15,6 +15,7 @@ module MergeRequests ...@@ -15,6 +15,7 @@ module MergeRequests
execute_hooks(merge_request, 'merge') execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees) invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
end end
private private
...@@ -31,6 +32,10 @@ module MergeRequests ...@@ -31,6 +32,10 @@ module MergeRequests
end end
end end
def delete_non_latest_diffs(merge_request)
DeleteNonLatestDiffsService.new(merge_request).execute
end
def create_merge_event(merge_request, current_user) def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user) EventCreateService.new.merge_mr(merge_request, current_user)
end end
......
module MergeRequests
class ReloadDiffsService
def initialize(merge_request, current_user)
@merge_request = merge_request
@current_user = current_user
end
def execute
old_diff_refs = merge_request.diff_refs
new_diff = merge_request.create_merge_request_diff
clear_cache(new_diff)
update_diff_discussion_positions(old_diff_refs)
end
private
attr_reader :merge_request, :current_user
def update_diff_discussion_positions(old_diff_refs)
new_diff_refs = merge_request.diff_refs
merge_request.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
current_user: current_user)
end
def clear_cache(new_diff)
# Executing the iteration we cache highlighted diffs for each diff file of
# MergeRequestDiff.
new_diff.diffs_collection.diff_files.to_a
# Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when
# reloading the diff.
MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
next if merge_request_diff == new_diff
merge_request_diff.diffs_collection.clear_cache!
end
end
end
end
...@@ -22,8 +22,10 @@ module Projects ...@@ -22,8 +22,10 @@ module Projects
) )
end end
def cache_key def cache_key(key = nil)
['projects', 'count_service', VERSION, @project.id, cache_key_name] cache_key = key || cache_key_name
['projects', 'count_service', VERSION, @project.id, cache_key]
end end
def self.query(project_ids) def self.query(project_ids)
......
...@@ -4,6 +4,10 @@ module Projects ...@@ -4,6 +4,10 @@ module Projects
class OpenIssuesCountService < Projects::CountService class OpenIssuesCountService < Projects::CountService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
# Cache keys used to store issues count
PUBLIC_COUNT_KEY = 'public_open_issues_count'.freeze
TOTAL_COUNT_KEY = 'total_open_issues_count'.freeze
def initialize(project, user = nil) def initialize(project, user = nil)
@user = user @user = user
...@@ -11,7 +15,7 @@ module Projects ...@@ -11,7 +15,7 @@ module Projects
end end
def cache_key_name def cache_key_name
public_only? ? 'public_open_issues_count' : 'total_open_issues_count' public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
end end
def public_only? def public_only?
...@@ -28,6 +32,32 @@ module Projects ...@@ -28,6 +32,32 @@ module Projects
end end
end end
def public_count_cache_key
cache_key(PUBLIC_COUNT_KEY)
end
def total_count_cache_key
cache_key(TOTAL_COUNT_KEY)
end
def refresh_cache(&block)
if block_given?
super(&block)
else
count_grouped_by_confidential = self.class.query(@project, public_only: false).group(:confidential).count
public_count = count_grouped_by_confidential[false] || 0
total_count = public_count + (count_grouped_by_confidential[true] || 0)
update_cache_for_key(public_count_cache_key) do
public_count
end
update_cache_for_key(total_count_cache_key) do
total_count
end
end
end
# We only show total issues count for reporters # We only show total issues count for reporters
# which are allowed to view confidential issues # which are allowed to view confidential issues
# This will still show a discrepancy on issues number but should be less than before. # This will still show a discrepancy on issues number but should be less than before.
......
...@@ -82,7 +82,7 @@ class WebHookService ...@@ -82,7 +82,7 @@ class WebHookService
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '') post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = { basic_auth = {
username: CGI.unescape(parsed_url.user), username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password) password: CGI.unescape(parsed_url.password.presence || '')
} }
make_request(post_url, basic_auth) make_request(post_url, basic_auth)
end end
......
...@@ -324,3 +324,6 @@ ...@@ -324,3 +324,6 @@
= _('Configure push mirrors.') = _('Configure push mirrors.')
.settings-content .settings-content
= render partial: 'repository_mirrors_form' = render partial: 'repository_mirrors_form'
= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
...@@ -10,16 +10,16 @@ ...@@ -10,16 +10,16 @@
.col-sm-10 .col-sm-10
= f.text_field :description, class: "form-control js-quick-submit" = f.text_field :description, class: "form-control js-quick-submit"
.form-group.row .form-group.row
= f.label :color, "Background color", class: 'col-form-label col-sm-2' = f.label :color, _("Background color"), class: 'col-form-label col-sm-2'
.col-sm-10 .col-sm-10
.input-group .input-group
.input-group-prepend .input-group-prepend
.input-group-text.label-color-preview &nbsp; .input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control" = f.text_field :color, class: "form-control"
.form-text.text-muted .form-text.text-muted
Choose any color. = _('Choose any color.')
%br %br
Or you can choose one of the suggested colors below = _("Or you can choose one of the suggested colors below")
.suggest-colors .suggest-colors
- suggested_colors.each do |color| - suggested_colors.each do |color|
...@@ -27,5 +27,5 @@ ...@@ -27,5 +27,5 @@
&nbsp; &nbsp;
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save js-save-button' = f.submit _('Save'), class: 'btn btn-save js-save-button'
= link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' = link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
= render_colored_label(label, tooltip: false) = render_colored_label(label, tooltip: false)
= markdown_field(label, :description) = markdown_field(label, :description)
.float-right .float-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to _('Edit'), edit_admin_label_path(label), class: 'btn btn-sm'
= link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} = link_to _('Delete'), admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
- add_to_breadcrumbs "Labels", admin_labels_path - add_to_breadcrumbs _("Labels"), admin_labels_path
- breadcrumb_title "Edit Label" - breadcrumb_title _("Edit Label")
- page_title "Edit", @label.name, "Labels" - page_title _("Edit"), @label.name, _("Labels")
%h3.page-title %h3.page-title
Edit Label = _('Edit Label')
%hr %hr
= render 'form' = render 'form'
- page_title "Labels" - page_title _("Labels")
%div %div
= link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do = link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do
New label = _('New label')
%h3.page-title %h3.page-title
Labels = _('Labels')
%hr %hr
.labels .labels
...@@ -14,5 +14,5 @@ ...@@ -14,5 +14,5 @@
= paginate @labels, theme: 'gitlab' = paginate @labels, theme: 'gitlab'
- else - else
.card.bg-light .card.bg-light
.nothing-here-block There are no labels yet .nothing-here-block= _('There are no labels yet')
- page_title "New Label" - page_title _("New Label")
%h3.page-title %h3.page-title
New Label = _('New Label')
%hr %hr
= render 'form' = render 'form'
...@@ -17,6 +17,11 @@ ...@@ -17,6 +17,11 @@
= link_to _("Help"), help_path = link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider %li.divider
%li
= link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
= _("Contribute to GitLab")
= icon('external-link')
%li.divider
- if current_user_menu?(:sign_out) - if current_user_menu?(:sign_out)
%li %li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link" = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.sidebar-context-title .sidebar-context-title
= @project.name = @project.name
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project' do = link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container .nav-icon-container
= sprite_icon('project') = sprite_icon('project')
...@@ -29,13 +29,13 @@ ...@@ -29,13 +29,13 @@
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span= _('Activity') %span= _('Activity')
= render_if_exists 'projects/sidebar/security_dashboard'
- if can?(current_user, :read_cycle_analytics, @project) - if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do = nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics') %span= _('Cycle Analytics')
= render_if_exists 'projects/sidebar/security_dashboard'
- if project_nav_tab? :files - if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths) do = nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree' do = link_to project_tree_path(@project), class: 'shortcuts-tree' do
......
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
.form-group .form-group
= f.label :key, class: 'label-light' = f.label :key, class: 'label-light'
= f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the SSH key. Paste the public part, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'." %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
= f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: 'Typically starts with "ssh-rsa …"'
.form-group .form-group
= f.label :title, class: 'label-light' = f.label :title, class: 'label-light'
= f.text_field :title, class: "form-control", required: true = f.text_field :title, class: "form-control", required: true, placeholder: 'e.g. My MacBook key'
%p.form-text.text-muted= _('Name your individual key via a title')
.prepend-top-default .prepend-top-default
= f.submit 'Add key', class: "btn btn-create" = f.submit 'Add key', class: "btn btn-create"
...@@ -11,10 +11,11 @@ ...@@ -11,10 +11,11 @@
%h5.prepend-top-0 %h5.prepend-top-0
Add an SSH key Add an SSH key
%p.profile-settings-content %p.profile-settings-content
Before you can add an SSH key you need to - generate_link_url = help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
= link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair') - existing_link_url = help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
or use an - generate_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_link_url }
= link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair') - existing_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: existing_link_url }
= _('To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}.').html_safe % { generate_link_start: generate_link_start, existing_link_start: existing_link_start, link_end: '</a>'.html_safe }
= render 'form' = render 'form'
%hr %hr
%h5 %h5
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= image_tag 'illustrations/logos/google-cloud-platform_logo.svg' = image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
.col-sm-10 .col-sm-10
%h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform') %h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' } %a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
Apply for credit Apply for credit
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
= author_avatar(deployment.commit, size: 20) = author_avatar(deployment.commit, size: 20)
= link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message" = link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
- else - else
Cant find HEAD commit for this branch = _("Can't find HEAD commit for this branch")
.gl-responsive-table-row.deployment{ role: 'row' } .gl-responsive-table-row.deployment{ role: 'row' }
.table-section.section-10{ role: 'gridcell' } .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } ID .table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid} %strong.table-mobile-content ##{deployment.iid}
.table-section.section-30{ role: 'gridcell' } .table-section.section-30{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Commit .table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment = render 'projects/deployments/commit', deployment: deployment
.table-section.section-25.build-column{ role: 'gridcell' } .table-section.section-25.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Job .table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable - if deployment.deployable
.table-mobile-content .table-mobile-content
.flex-truncate-parent .flex-truncate-parent
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= user_avatar(user: deployment.user, size: 20) = user_avatar(user: deployment.user, size: 20)
.table-section.section-15{ role: 'gridcell' } .table-section.section-15{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Created .table-mobile-header{ role: 'rowheader' }= _("Created")
%span.table-mobile-content= time_ago_with_tooltip(deployment.created_at) %span.table-mobile-content= time_ago_with_tooltip(deployment.created_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' } .table-section.section-20.table-button-footer{ role: 'gridcell' }
......
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment) && deployment.deployable
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last? - if deployment.last?
Re-deploy = _("Re-deploy")
- else - else
Rollback = _("Rollback")
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
#{@commits_graph.start_date.strftime('%b %d')} #{@commits_graph.start_date.strftime('%b %d')}
- end_time = capture do - end_time = capture do
#{@commits_graph.end_date.strftime('%b %d')} #{@commits_graph.end_date.strftime('%b %d')}
= (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{@ref}</strong>", start_time: start_time, end_time: end_time }).html_safe = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{h @ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
.col-md-6 .col-md-6
.tree-ref-container .tree-ref-container
......
...@@ -16,6 +16,6 @@ ...@@ -16,6 +16,6 @@
%span.ref-name= @merge_request.target_branch %span.ref-name= @merge_request.target_branch
.text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save' .text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
- else - else
- diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true - diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true
- if diff_viewable - if diff_viewable
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
- @no_breadcrumb_container = true - @no_breadcrumb_container = true
- @no_container = true - @no_container = true
- @content_class = "issue-boards-content" - @content_class = "issue-boards-content"
- breadcrumb_title "Issue Board" - breadcrumb_title _("Issue Board")
- page_title "Boards" - page_title _("Boards")
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%board-delete{ "inline-template" => true, %board-delete{ "inline-template" => true,
":list" => "list", ":list" => "list",
"v-if" => "!list.preset && list.id" } "v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.float-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash") = icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' } .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } %span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
...@@ -39,8 +39,8 @@ ...@@ -39,8 +39,8 @@
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button", %button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm", "@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"', "v-if" => 'list.type !== "closed"',
"aria-label" => "New issue", "aria-label" => _("New issue"),
"title" => "New issue", "title" => _("New issue"),
data: { placement: "top", container: "body" } } data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse") = icon("plus", class: "js-no-trigger-collapse")
......
.block.due_date .block.due_date
.title .title
Due date = _("Due date")
- if can_admin_issue? - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right" = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value .value
.value-content .value-content
%span.no-value{ "v-if" => "!issue.dueDate" } %span.no-value{ "v-if" => "!issue.dueDate" }
No due date = _("No due date")
%span.bold{ "v-if" => "issue.dueDate" } %span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }} {{ issue.dueDate | due-date }}
- if can_admin_issue? - if can_admin_issue?
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" } %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
\- \-
%a.js-remove-due-date{ href: "#", role: "button" } %a.js-remove-due-date{ href: "#", role: "button" }
remove due date = _('remove due date')
- if can_admin_issue? - if can_admin_issue?
.selectbox .selectbox
%input{ type: "hidden", %input{ type: "hidden",
...@@ -23,9 +23,9 @@ ...@@ -23,9 +23,9 @@
.dropdown .dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button', %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } } data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } }
%span.dropdown-toggle-text Due date %span.dropdown-toggle-text= _("Due date")
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date .dropdown-menu.dropdown-menu-due-date
= dropdown_title('Due date') = dropdown_title(_('Due date'))
= dropdown_content do = dropdown_content do
.js-due-date-calendar .js-due-date-calendar
.block.labels .block.labels
.title .title
Labels = _("Labels")
- if can_admin_issue? - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right" = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value.issuable-show-labels.dont-hide .value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None = _("None")
%a{ href: "#", %a{ href: "#",
"v-for" => "label in issue.labels" } "v-for" => "label in issue.labels" }
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project.try(:path) } } project_path: @project.try(:path) } }
%span.dropdown-toggle-text %span.dropdown-toggle-text
Label = _("Label")
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default" = render partial: "shared/issuable/label_page_default"
......
.block.milestone .block.milestone
.title .title
Milestone = _("Milestone")
- if can_admin_issue? - if can_admin_issue?
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right" = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value .value
%span.no-value{ "v-if" => "!issue.milestone" } %span.no-value{ "v-if" => "!issue.milestone" }
None = _("None")
%span.bold.has-tooltip{ "v-if" => "issue.milestone" } %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
{{ issue.milestone.title }} {{ issue.milestone.title }}
- if can_admin_issue? - if can_admin_issue?
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" }, %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle", ":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.iid" } ":data-issuable-id" => "issue.iid" }
Milestone = _("Milestone")
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
= dropdown_title("Assign milestone") = dropdown_title(_("Assign milestone"))
= dropdown_filter("Search milestones") = dropdown_filter(_("Search milestones"))
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
...@@ -40,5 +40,5 @@ ...@@ -40,5 +40,5 @@
= yield(:note_actions) = yield(:note_actions)
%a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Discard draft" } }
Discard draft Discard draft
...@@ -6,5 +6,4 @@ ...@@ -6,5 +6,4 @@
%fieldset %fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light" = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light"
%span= t(scope, scope: [:doorkeeper, :scopes])
.scope-description= t scope, scope: [:doorkeeper, :scope_desc] .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code %a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-authenticate-u2f-in-progress{ type: "text/template" } %script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
......
...@@ -118,3 +118,4 @@ ...@@ -118,3 +118,4 @@
- web_hook - web_hook
- repository_update_remote_mirror - repository_update_remote_mirror
- create_note_diff_file - create_note_diff_file
- delete_diff_files
class DeleteDiffFilesWorker
include ApplicationWorker
def perform(merge_request_diff_id)
merge_request_diff = MergeRequestDiff.find(merge_request_diff_id)
return if merge_request_diff.without_files?
MergeRequestDiff.transaction do
merge_request_diff.clean!
MergeRequestDiffFile
.where(merge_request_diff_id: merge_request_diff.id)
.delete_all
end
end
end
...@@ -19,7 +19,24 @@ Options = Struct.new( ...@@ -19,7 +19,24 @@ Options = Struct.new(
) )
INVALID_TYPE = -1 INVALID_TYPE = -1
module ChangelogHelpers
Abort = Class.new(StandardError)
Done = Class.new(StandardError)
def capture_stdout(cmd)
output = IO.popen(cmd, &:read)
fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
output
end
def fail_with(message)
raise Abort, "\e[31merror\e[0m #{message}"
end
end
class ChangelogOptionParser class ChangelogOptionParser
extend ChangelogHelpers
Type = Struct.new(:name, :description) Type = Struct.new(:name, :description)
TYPES = [ TYPES = [
Type.new('added', 'New feature'), Type.new('added', 'New feature'),
...@@ -68,7 +85,7 @@ class ChangelogOptionParser ...@@ -68,7 +85,7 @@ class ChangelogOptionParser
opts.on('-h', '--help', 'Print help message') do opts.on('-h', '--help', 'Print help message') do
$stdout.puts opts $stdout.puts opts
exit raise Done.new
end end
end end
...@@ -108,18 +125,19 @@ class ChangelogOptionParser ...@@ -108,18 +125,19 @@ class ChangelogOptionParser
def assert_valid_type!(type) def assert_valid_type!(type)
unless type unless type
$stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}" raise Abort, "Invalid category index, please select an index between 1 and #{TYPES.length}"
exit 1
end end
end end
def git_user_name def git_user_name
%x{git config user.name}.strip capture_stdout(%w[git config user.name]).strip
end end
end end
end end
class ChangelogEntry class ChangelogEntry
include ChangelogHelpers
attr_reader :options attr_reader :options
def initialize(options) def initialize(options)
...@@ -159,13 +177,9 @@ class ChangelogEntry ...@@ -159,13 +177,9 @@ class ChangelogEntry
end end
def amend_commit def amend_commit
%x{git add #{file_path}} fail_with "git add failed" unless system(*%W[git add #{file_path}])
exec("git commit --amend")
end
def fail_with(message) Kernel.exec(*%w[git commit --amend])
$stderr.puts "\e[31merror\e[0m #{message}"
exit 1
end end
def assert_feature_branch! def assert_feature_branch!
...@@ -203,7 +217,7 @@ class ChangelogEntry ...@@ -203,7 +217,7 @@ class ChangelogEntry
end end
def last_commit_subject def last_commit_subject
%x{git log --format="%s" -1}.strip capture_stdout(%w[git log --format=%s -1]).strip
end end
def file_path def file_path
...@@ -225,7 +239,7 @@ class ChangelogEntry ...@@ -225,7 +239,7 @@ class ChangelogEntry
end end
def branch_name def branch_name
@branch_name ||= %x{git symbolic-ref --short HEAD}.strip @branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
end end
def remove_trailing_whitespace(yaml_content) def remove_trailing_whitespace(yaml_content)
...@@ -234,8 +248,15 @@ class ChangelogEntry ...@@ -234,8 +248,15 @@ class ChangelogEntry
end end
if $0 == __FILE__ if $0 == __FILE__
options = ChangelogOptionParser.parse(ARGV) begin
ChangelogEntry.new(options) options = ChangelogOptionParser.parse(ARGV)
ChangelogEntry.new(options)
rescue ChangelogHelpers::Abort => ex
$stderr.puts ex.message
exit 1
rescue ChangelogHelpers::Done
exit
end
end end
# vim: ft=ruby # vim: ft=ruby
---
title: Improve U2F workflow when using unsupported browsers
merge_request: 19938
author: Jan Beckmann
type: changed
---
title: Fade uneditable area in Web IDE
merge_request: 20008
author:
type: changed
---
title: Update Web IDE file tree styles
merge_request: 19969
author:
type: changed
---
title: Update new SSH key page to improve copy
merge_request: 19994
author:
type: other
---
title: Fix webhook error when password is not present
merge_request: 19945
author: Jan Beckmann
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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