Merge branch 'master' into ide-temp-file-folder-fixes

This commit is contained in:
Phil Hughes 2018-04-25 10:51:41 +01:00
commit 9990afb92c
No known key found for this signature in database
GPG Key ID: 32245528C52E0F9F
384 changed files with 4418 additions and 1854 deletions

View File

@ -1,6 +1,9 @@
{ {
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"], "presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": { "env": {
"karma": {
"plugins": ["rewire"]
},
"coverage": { "coverage": {
"plugins": [ "plugins": [
[ [
@ -14,7 +17,8 @@
{ {
"process.env.BABEL_ENV": "coverage" "process.env.BABEL_ENV": "coverage"
} }
] ],
"rewire"
] ]
} }
} }

View File

@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral: Lint/NestedPercentLiteral:
Exclude: Exclude:
- 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb' - 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb' - 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb' - 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb' - 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb' - 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/seed_repo.rb' - 'spec/support/helpers/seed_repo.rb'
# Offense count: 112 # Offense count: 112
# Configuration parameters: Blacklist. # Configuration parameters: Blacklist.
@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb' - 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb' - 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb' - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102 # Offense count: 102
# Cop supports --auto-correct. # Cop supports --auto-correct.

View File

@ -2,6 +2,34 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.7.1 (2018-04-23)
### Fixed (11 changes)
- [API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` when no value is passed for `order_by` or `sort`. !18393
- Fix a case with secret variables being empty sometimes. !18400
- Fix `Trace::HttpIO` can not render multi-byte chars. !18417
- Fix specifying a non-default ref when requesting an archive using the legacy URL. !18468
- Respect visibility options and description when importing project from template. !18473
- Removes 'No Job log' message from build trace. !18523
- Align action icons in pipeline graph.
- Fix direct_upload when records with null file_store are used.
- Removed alert box in IDE when redirecting to new merge request.
- Fixed IDE not loading for sub groups.
- Fixed IDE not showing loading state when tree is loading.
### Performance (4 changes)
- Validate project path prior to hitting the database. !18322
- Add index to file_store on ci_job_artifacts. !18444
- Fix N+1 queries when loading participants for a commit note.
- Support Markdown rendering using multiple projects.
### Added (1 change)
- Add an API endpoint to download git repository snapshots. !18173
## 10.7.0 (2018-04-22) ## 10.7.0 (2018-04-22)
### Security (6 changes, 2 of them are from the community) ### Security (6 changes, 2 of them are from the community)

View File

@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release) - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release" - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the All labels, their meaning and priority are defined on the
[labels page][labels-page]. [labels page][labels-page].
@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| Label | Meaning | Estimate time to fix | Guidance | | Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------| |-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release | | | ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | | | ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |

View File

@ -178,7 +178,7 @@ GEM
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.1) doorkeeper (4.3.2)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.3.0) doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
@ -483,10 +483,11 @@ GEM
logging (2.2.2) logging (2.2.2)
little-plugger (~> 1.1) little-plugger (~> 1.1)
multi_json (~> 1.10) multi_json (~> 1.10)
lograge (0.5.1) lograge (0.10.0)
actionpack (>= 4, < 5.2) actionpack (>= 4)
activesupport (>= 4, < 5.2) activesupport (>= 4)
railties (>= 4, < 5.2) railties (>= 4)
request_store (~> 1.0)
loofah (2.2.2) loofah (2.2.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)

View File

@ -1 +1 @@
10.7.0-pre 10.8.0-pre

View File

@ -0,0 +1,245 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue';
import router from '../../ide_router';
import {
MAX_FILE_FINDER_RESULTS,
FILE_FINDER_ROW_HEIGHT,
FILE_FINDER_EMPTY_ROW_HEIGHT,
} from '../../constants';
import {
UP_KEY_CODE,
DOWN_KEY_CODE,
ENTER_KEY_CODE,
ESC_KEY_CODE,
} from '../../../lib/utils/keycodes';
export default {
components: {
Item,
VirtualList,
},
data() {
return {
focusedIndex: 0,
searchText: '',
mouseOver: false,
cancelMouseOver: false,
};
},
computed: {
...mapGetters(['allBlobs']),
...mapState(['fileFindVisible', 'loading']),
filteredBlobs() {
const searchText = this.searchText.trim();
if (searchText === '') {
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
}
return fuzzaldrinPlus
.filter(this.allBlobs, searchText, {
key: 'path',
maxResults: MAX_FILE_FINDER_RESULTS,
})
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
},
filteredBlobsLength() {
return this.filteredBlobs.length;
},
listShowCount() {
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
},
listHeight() {
return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
},
showClearInputButton() {
return this.searchText.trim() !== '';
},
},
watch: {
fileFindVisible() {
this.$nextTick(() => {
if (!this.fileFindVisible) {
this.searchText = '';
} else {
this.focusedIndex = 0;
if (this.$refs.searchInput) {
this.$refs.searchInput.focus();
}
}
});
},
searchText() {
this.focusedIndex = 0;
},
focusedIndex() {
if (!this.mouseOver) {
this.$nextTick(() => {
const el = this.$refs.virtualScrollList.$el;
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
if (this.focusedIndex === 0) {
// if index is the first index, scroll straight to start
el.scrollTop = 0;
} else if (this.focusedIndex === this.filteredBlobsLength - 1) {
// if index is the last index, scroll to the end
el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
} else if (scrollTop >= bottom + el.scrollTop) {
// if element is off the bottom of the scroll list, scroll down one item
el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
} else if (scrollTop < el.scrollTop) {
// if element is off the top of the scroll list, scroll up one item
el.scrollTop = scrollTop;
}
});
}
},
},
methods: {
...mapActions(['toggleFileFinder']),
clearSearchInput() {
this.searchText = '';
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
},
onKeydown(e) {
switch (e.keyCode) {
case UP_KEY_CODE:
e.preventDefault();
this.mouseOver = false;
this.cancelMouseOver = true;
if (this.focusedIndex > 0) {
this.focusedIndex -= 1;
} else {
this.focusedIndex = this.filteredBlobsLength - 1;
}
break;
case DOWN_KEY_CODE:
e.preventDefault();
this.mouseOver = false;
this.cancelMouseOver = true;
if (this.focusedIndex < this.filteredBlobsLength - 1) {
this.focusedIndex += 1;
} else {
this.focusedIndex = 0;
}
break;
default:
break;
}
},
onKeyup(e) {
switch (e.keyCode) {
case ENTER_KEY_CODE:
this.openFile(this.filteredBlobs[this.focusedIndex]);
break;
case ESC_KEY_CODE:
this.toggleFileFinder(false);
break;
default:
break;
}
},
openFile(file) {
this.toggleFileFinder(false);
router.push(`/project${file.url}`);
},
onMouseOver(index) {
if (!this.cancelMouseOver) {
this.mouseOver = true;
this.focusedIndex = index;
}
},
onMouseMove(index) {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
},
};
</script>
<template>
<div
class="ide-file-finder-overlay"
@mousedown.self="toggleFileFinder(false)"
>
<div
class="dropdown-menu diff-file-changes ide-file-finder show"
>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
:placeholder="__('Search files')"
autocomplete="off"
v-model="searchText"
ref="searchInput"
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
:class="{
hidden: showClearInputButton
}"
></i>
<i
role="button"
:aria-label="__('Clear search input')"
class="fa fa-times dropdown-input-clear"
:class="{
show: showClearInputButton
}"
@click="clearSearchInput"
></i>
</div>
<div>
<virtual-list
:size="listHeight"
:remain="listShowCount"
wtag="ul"
ref="virtualScrollList"
>
<template v-if="filteredBlobsLength">
<li
v-for="(file, index) in filteredBlobs"
:key="file.key"
>
<item
class="disable-hover"
:file="file"
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
@click="openFile"
@mouseover="onMouseOver"
@mousemove="onMouseMove"
/>
</li>
</template>
<li
v-else
class="dropdown-menu-empty-item"
>
<div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
<template v-if="loading">
{{ __('Loading...') }}
</template>
<template v-else>
{{ __('No files found.') }}
</template>
</div>
</li>
</virtual-list>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,113 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
export default {
components: {
ChangedFileIcon,
FileIcon,
},
props: {
file: {
type: Object,
required: true,
},
focused: {
type: Boolean,
required: true,
},
searchText: {
type: String,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
pathWithEllipsis() {
const path = this.file.path;
return path.length < MAX_PATH_LENGTH
? path
: `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
},
nameSearchTextOccurences() {
return fuzzaldrinPlus.match(this.file.name, this.searchText);
},
pathSearchTextOccurences() {
return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
},
},
methods: {
clickRow() {
this.$emit('click', this.file);
},
mouseOverRow() {
this.$emit('mouseover', this.index);
},
mouseMove() {
this.$emit('mousemove', this.index);
},
},
};
</script>
<template>
<button
type="button"
class="diff-changed-file"
:class="{
'is-focused': focused,
}"
@click.prevent="clickRow"
@mouseover="mouseOverRow"
@mousemove="mouseMove"
>
<file-icon
:file-name="file.name"
:size="16"
css-classes="diff-file-changed-icon append-right-8"
/>
<span class="diff-changed-file-content append-right-8">
<strong
class="diff-changed-file-name"
>
<span
v-for="(char, index) in file.name.split('')"
:key="index + char"
:class="{
highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
}"
v-text="char"
>
</span>
</strong>
<span
class="diff-changed-file-path prepend-top-5"
>
<span
v-for="(char, index) in pathWithEllipsis.split('')"
:key="index + char"
:class="{
highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
}"
v-text="char"
>
</span>
</span>
</span>
<span
v-if="file.changed || file.tempFile"
class="diff-changed-stats"
>
<changed-file-icon
:file="file"
/>
</span>
</button>
</template>

View File

@ -1,55 +1,91 @@
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import Mousetrap from 'mousetrap';
import ideContextbar from './ide_context_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import repoTabs from './repo_tabs.vue'; import ideContextbar from './ide_context_bar.vue';
import ideStatusBar from './ide_status_bar.vue'; import repoTabs from './repo_tabs.vue';
import repoEditor from './repo_editor.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
export default { const originalStopCallback = Mousetrap.stopCallback;
components: {
ideSidebar,
ideContextbar,
repoTabs,
ideStatusBar,
repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, { export default {
returnValue, components: {
ideSidebar,
ideContextbar,
repoTabs,
ideStatusBar,
repoEditor,
FindFile,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
}); });
return returnValue;
}; Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
}, },
}; methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
},
};
</script> </script>
<template> <template>
<div <div
class="ide-view" class="ide-view"
> >
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar /> <ide-sidebar />
<div <div
class="multi-file-edit-pane" class="multi-file-edit-pane"

View File

@ -1,3 +1,8 @@
// Fuzzy file finder // Fuzzy file finder
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
// Commit message textarea
export const MAX_TITLE_LENGTH = 50; export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72; export const MAX_BODY_LENGTH = 72;

View File

@ -1,10 +1,12 @@
import _ from 'underscore'; import _ from 'underscore';
import store from '../stores';
import DecorationsController from './decorations/controller'; import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller'; import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable'; import Disposable from './common/disposable';
import ModelManager from './common/model_manager'; import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options'; import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme'; import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json';
export const clearDomElement = el => { export const clearDomElement = el => {
if (!el || !el.firstChild) return; if (!el || !el.firstChild) return;
@ -53,6 +55,8 @@ export default class Editor {
)), )),
); );
this.addCommands();
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
} }
} }
@ -73,6 +77,8 @@ export default class Editor {
})), })),
); );
this.addCommands();
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
} }
} }
@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) { static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700; return domElement.offsetWidth >= 700;
} }
addCommands() {
const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0;
return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
};
keymap.forEach(command => {
const keybindings = command.bindings.map(binding => {
const keys = binding.split('+');
// eslint-disable-next-line no-bitwise
return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
});
this.instance.addAction({
id: command.id,
label: command.label,
keybindings,
run() {
store.dispatch(command.action.name, command.action.params);
return null;
},
});
});
}
} }

View File

@ -0,0 +1,11 @@
[
{
"id": "file-finder",
"label": "File finder",
"bindings": ["CtrlCmd+KEY_P"],
"action": {
"name": "toggleFileFinder",
"params": true
}
}
]

View File

@ -146,7 +146,13 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp
} }
}; };
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
export * from './actions/merge_request'; export * from './actions/merge_request';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -42,4 +42,20 @@ export const collapseButtonTooltip = state =>
export const hasMergeRequest = state => !!state.currentMergeRequestId; export const hasMergeRequest = state => !!state.currentMergeRequestId;
export const allBlobs = state =>
Object.keys(state.entries)
.reduce((acc, key) => {
const entry = state.entries[key];
if (entry.type === 'blob') {
acc.push(entry);
}
return acc;
}, [])
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -196,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(types.UPDATE_LOADING, false); commit(types.UPDATE_LOADING, false);
}); });
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId; return rootState.currentBranchId;
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -60,3 +60,4 @@ export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';

View File

@ -107,6 +107,11 @@ export default {
delayViewerUpdated, delayViewerUpdated,
}); });
}, },
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, {
fileFindVisible,
});
},
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path); const changedFile = state.changedFiles.find(f => f.path === file.path);

View File

@ -4,6 +4,7 @@ export default {
[types.SET_FILE_ACTIVE](state, { path, active }) { [types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
active, active,
lastOpenedAt: new Date().getTime(),
}); });
if (active && !state.entries[path].pending) { if (active && !state.entries[path].pending) {

View File

@ -18,4 +18,5 @@ export default () => ({
entries: {}, entries: {},
viewer: 'editor', viewer: 'editor',
delayViewerUpdated: false, delayViewerUpdated: false,
fileFindVisible: false,
}); });

View File

@ -43,6 +43,7 @@ export const dataStructure = () => ({
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0, size: 0,
lastOpenedAt: 0,
}); });
export const decorateData = entity => { export const decorateData = entity => {

View File

@ -45,7 +45,7 @@ export default {
return timeIntervalInWords(this.job.queued); return timeIntervalInWords(this.job.queued);
}, },
runnerId() { runnerId() {
return `#${this.job.runner.id}`; return `${this.job.runner.description} (#${this.job.runner.id})`;
}, },
retryButtonClass() { retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block'; let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';

View File

@ -0,0 +1,4 @@
export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40;
export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27;

View File

@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el); scrollToElement(el);
} }
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length; return Object.keys(resolvedMap).length;
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -32,26 +32,38 @@ export default {
required: true, required: true,
}, },
buttonDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
isDisabled() { },
return this.buttonDisabled === this.link; watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
}, },
}, },
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
}, },
}, },
}; };
@ -62,7 +74,8 @@ export default {
@click="onClickAction" @click="onClickAction"
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled" :disabled="isDisabled"

View File

@ -1,53 +0,0 @@
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>

View File

@ -1,77 +1,83 @@
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the dropdown for the pipeline graph. * Renders the dropdown for the pipeline graph.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
* *
* { * {
* "id": 4256, * "id": 4256,
* "name": "test", * "name": "test",
* "status": { * "status": {
* "icon": "icon_status_success", * "icon": "icon_status_success",
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
* } * }
* } * }
* } * }
*/ */
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
JobComponent,
JobNameComponent,
},
props: {
job: {
type: Object,
required: true,
}, },
requestFinishedFor: {
components: { type: String,
jobComponent, required: false,
jobNameComponent, default: '',
}, },
},
props: { computed: {
job: { tooltipText() {
type: Object, return `${this.job.name} - ${this.job.status.label}`;
required: true,
},
}, },
},
computed: { mounted() {
tooltipText() { this.stopDropdownClickPropagation();
return `${this.job.name} - ${this.job.status.label}`; },
},
},
mounted() { methods: {
this.stopDropdownClickPropagation(); /**
}, * When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed so we stop propagation
methods: { * of the click event inside the dropdown.
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
* *
* Since this component is rendered multiple times per page we need to guarantee we only * Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el $(
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
.on('click', (e) => { this.$el,
e.stopPropagation(); ).on('click', e => {
}); e.stopPropagation();
}, });
}, },
}; },
};
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container"> <div class="ci-job-dropdown-container">
@ -101,8 +107,8 @@
:key="i"> :key="i">
<job-component <job-component
:job="item" :job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
</ul> </ul>

View File

@ -7,7 +7,6 @@ export default {
StageColumnComponent, StageColumnComponent,
LoadingIcon, LoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
@ -17,10 +16,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
actionDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
@ -75,7 +74,7 @@ export default {
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled" :request-finished-for="requestFinishedFor"
/> />
</ul> </ul>
</div> </div>

View File

@ -1,6 +1,5 @@
<script> <script>
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
ActionComponent, ActionComponent,
DropdownActionComponent,
JobNameComponent, JobNameComponent,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
@ -44,26 +41,17 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
cssClassJobName: { cssClassJobName: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
computed: { computed: {
status() { status() {
return this.job && this.job.status ? this.job.status : {}; return this.job && this.job.status ? this.job.status : {};
@ -134,19 +122,11 @@ export default {
</div> </div>
<action-component <action-component
v-if="hasAction && !isDropdown" v-if="hasAction"
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:button-disabled="actionDisabled" :request-finished-for="requestFinishedFor"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:action-method="status.action.method"
/> />
</div> </div>
</template> </template>

View File

@ -29,10 +29,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
actionDisabled: {
requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
@ -74,12 +75,12 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>

View File

@ -25,7 +25,7 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null, requestFinishedFor: null,
}; };
}, },
created() { created() {
@ -36,15 +36,17 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.actionDisabled = action; // Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action) this.mediator.service
.postAction(action)
.then(() => { .then(() => {
this.mediator.refreshPipeline(); this.mediator.refreshPipeline();
this.actionDisabled = null; this.requestFinishedFor = action;
}) })
.catch(() => { .catch(() => {
this.actionDisabled = null; this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.')); Flash(__('An error occurred while making the request.'));
}); });
}, },
@ -54,7 +56,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled, requestFinishedFor: this.requestFinishedFor,
}, },
}); });
}, },
@ -79,7 +81,8 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.mediator.service.postAction(action.path) this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },

View File

@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -1,2 +1,5 @@
export const isLoading = state => state.isLoading; export const isLoading = state => state.isLoading;
export const repos = state => state.repos; export const repos = state => state.repos;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -1,52 +1,52 @@
<script> <script>
import ciIcon from './ci_icon.vue'; import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
/** /**
* Renders CI Badge link with CI icon and status text based on * Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used. * API response shared between all places where it is used.
* *
* Receives status object containing: * Receives status object containing:
* status: { * status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class * group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon * icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip * label:"running" // used for potential tooltip
* text:"running" // text rendered * text:"running" // text rendered
* } * }
* *
* Used in: * Used in:
* - Pipelines table - first column * - Pipelines table - first column
* - Jobs table - first column * - Jobs table - first column
* - Pipeline show view - header * - Pipeline show view - header
* - Job show view - header * - Job show view - header
* - MR widget * - MR widget
*/ */
export default { export default {
components: { components: {
ciIcon, CiIcon,
},
directives: {
tooltip,
},
props: {
status: {
type: Object,
required: true,
}, },
directives: { showText: {
tooltip, type: Boolean,
required: false,
default: true,
}, },
props: { },
status: { computed: {
type: Object, cssClass() {
required: true, const className = this.status.group;
}, return className ? `ci-status ci-${className}` : 'ci-status';
showText: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { },
cssClass() { };
const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
};
</script> </script>
<template> <template>
<a <a

View File

@ -1,45 +1,44 @@
<script> <script>
import icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
/** /**
* Renders CI icon based on API response shared between all places where it is used. * Renders CI icon based on API response shared between all places where it is used.
* *
* Receives status object containing: * Receives status object containing:
* status: { * status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class * group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon * icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip * label:"running" // used for potential tooltip
* text:"running" // text rendered * text:"running" // text rendered
* } * }
* *
* Used in: * Used in:
* - Pipelines table Badge * - Pipelines table Badge
* - Pipelines table mini graph * - Pipelines table mini graph
* - Pipeline graph * - Pipeline graph
* - Pipeline show view badge * - Pipeline show view badge
* - Jobs table * - Jobs table
* - Jobs show view header * - Jobs show view header
* - Jobs show view sidebar * - Jobs show view sidebar
*/ */
export default { export default {
components: { components: {
icon, Icon,
},
props: {
status: {
type: Object,
required: true,
}, },
props: { },
status: { computed: {
type: Object, cssClass() {
required: true, const status = this.status.group;
}, return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
}, },
},
computed: { };
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
};
</script> </script>
<template> <template>
<span :class="cssClass"> <span :class="cssClass">

View File

@ -1,40 +1,50 @@
<script> <script>
/** /**
* Falls back to the code used in `copy_to_clipboard.js` * Falls back to the code used in `copy_to_clipboard.js`
*/ *
import tooltip from '../directives/tooltip'; * Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
* when clicked.
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
*/
import tooltip from '../directives/tooltip';
export default { export default {
name: 'ClipboardButton', name: 'ClipboardButton',
directives: { directives: {
tooltip, tooltip,
},
props: {
text: {
type: String,
required: true,
}, },
props: { title: {
text: { type: String,
type: String, required: true,
required: true,
},
title: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: [String, Boolean],
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn-default',
},
}, },
}; tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: [String, Boolean],
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn-default',
},
},
};
</script> </script>
<template> <template>

View File

@ -1,119 +1,111 @@
<script> <script>
import commitIconSvg from 'icons/_icon_commit.svg'; import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip';
import tooltip from '../directives/tooltip'; import Icon from '../../vue_shared/components/icon.vue';
import icon from '../../vue_shared/components/icon.vue';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
UserAvatarLink,
Icon,
},
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
required: false,
default: false,
}, },
components: { /**
userAvatarLink, * If provided is used to render the branch name and url.
icon, * Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
}, },
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/** /**
* Used to show the commit short sha that links to the commit url. * Used to show the commit short sha that links to the commit url.
*/ */
shortSha: { shortSha: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
showBranch: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { /**
/** * If provided shows the commit tile.
* Used to verify if all the properties needed to render the commit */
* ref section were provided. title: {
* type: String,
* @returns {Boolean} required: false,
*/ default: '',
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
}, },
created() { /**
this.commitIconSvg = commitIconSvg; * If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
}, },
}; showBranch: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
},
},
};
</script> </script>
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
@ -141,11 +133,10 @@
{{ commitRef.name }} {{ commitRef.name }}
</a> </a>
</template> </template>
<div <icon
v-html="commitIconSvg" name="commit"
class="commit-icon js-commit-icon" class="commit-icon js-commit-icon"
> />
</div>
<a <a
class="commit-sha" class="commit-sha"

View File

@ -1,33 +1,33 @@
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
/** /**
* Port of detail_behavior expand button. * Port of detail_behavior expand button.
* *
* @example * @example
* <expand-button> * <expand-button>
* <template slot="expanded"> * <template slot="expanded">
* Text goes here. * Text goes here.
* </template> * </template>
* </expand-button> * </expand-button>
*/ */
export default { export default {
name: 'ExpandButton', name: 'ExpandButton',
data() { data() {
return { return {
isCollapsed: true, isCollapsed: true,
}; };
},
computed: {
ariaLabel() {
return __('Click to expand text');
}, },
computed: { },
ariaLabel() { methods: {
return __('Click to expand text'); onClick() {
}, this.isCollapsed = !this.isCollapsed;
}, },
methods: { },
onClick() { };
this.isCollapsed = !this.isCollapsed;
},
},
};
</script> </script>
<template> <template>
<span> <span>

View File

@ -1,78 +1,78 @@
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import CiIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue'; import LoadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
components: { components: {
ciIconBadge, CiIconBadge,
loadingIcon, LoadingIcon,
timeagoTooltip, TimeagoTooltip,
userAvatarImage, UserAvatarImage,
},
directives: {
tooltip,
},
props: {
status: {
type: Object,
required: true,
}, },
directives: { itemName: {
tooltip, type: String,
required: true,
}, },
props: { itemId: {
status: { type: Number,
type: Object, required: true,
required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
}, },
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
},
computed: { computed: {
userAvatarAltText() { userAvatarAltText() {
return `${this.user.name}'s avatar`; return `${this.user.name}'s avatar`;
},
}, },
},
methods: { methods: {
onClickAction(action) { onClickAction(action) {
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
},
}, },
}; },
};
</script> </script>
<template> <template>

View File

@ -1,76 +1,75 @@
<script> <script>
/* This is a re-usable vue component for rendering a svg sprite
icon
/* This is a re-usable vue component for rendering a svg sprite Sample configuration:
icon
Sample configuration: <icon
name="retry"
:size="32"
css-classes="top"
/>
<icon */
name="retry" // only allow classes in images.scss e.g. s12
:size="32" const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
css-classes="top"
/>
*/ export default {
// only allow classes in images.scss e.g. s12 props: {
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; name: {
type: String,
required: true,
},
export default { size: {
props: { type: Number,
name: { required: false,
type: String, default: 16,
required: true, validator(value) {
}, return validSizes.includes(value);
size: {
type: Number,
required: false,
default: 16,
validator(value) {
return validSizes.includes(value);
},
},
cssClasses: {
type: String,
required: false,
default: '',
},
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
}, },
}, },
computed: { cssClasses: {
spriteHref() { type: String,
return `${gon.sprite_icons}#${this.name}`; required: false,
}, default: '',
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
}, },
};
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
},
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
},
};
</script> </script>
<template> <template>
@ -79,7 +78,8 @@
:width="width" :width="width"
:height="height" :height="height"
:x="x" :x="x"
:y="y"> :y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" /> <use v-bind="{ 'xlink:href':spriteHref }" />
</svg> </svg>
</template> </template>

View File

@ -1,4 +1,5 @@
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue'; import LoadingIcon from '../../loading_icon.vue';
@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick, handleClick: this.handleClick,
}); });
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
}, },
methods: { methods: {
handleClick(label) { handleClick(label) {
this.$emit('onLabelClick', label); this.$emit('onLabelClick', label);
}, },
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
}, },
}; };
</script> </script>
@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="showCreate" v-if="showCreate"
:labels="context.labels" :labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/> />
<dropdown-title <dropdown-title
:can-edit="canEdit" :can-edit="canEdit"
@ -133,7 +142,10 @@ export default {
:name="hiddenInputName" :name="hiddenInputName"
:label="label" :label="label"
/> />
<div class="dropdown"> <div
class="dropdown"
ref="dropdown"
>
<dropdown-button <dropdown-button
:ability-name="abilityName" :ability-name="abilityName"
:field-name="hiddenInputName" :field-name="hiddenInputName"

View File

@ -26,6 +26,11 @@ export default {
return labelsString; return labelsString;
}, },
}, },
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
}; };
</script> </script>
@ -36,6 +41,7 @@ export default {
data-placement="left" data-placement="left"
data-container="body" data-container="body"
:title="labelsList" :title="labelsList"
@click="handleClick"
> >
<i <i
aria-hidden="true" aria-hidden="true"

View File

@ -472,6 +472,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; } .append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; } .append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; } .append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; } .append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; } .append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; } .append-bottom-20 { margin-bottom: 20px; }

View File

@ -43,7 +43,7 @@
border-color: $gray-darkest; border-color: $gray-darkest;
} }
[data-toggle="dropdown"] { [data-toggle='dropdown'] {
outline: 0; outline: 0;
} }
} }
@ -172,7 +172,11 @@
color: $brand-danger; color: $brand-danger;
} }
&:hover, &.disable-hover {
text-decoration: none;
}
&:not(.disable-hover):hover,
&:active, &:active,
&:focus, &:focus,
&.is-focused { &.is-focused {
@ -508,17 +512,16 @@
} }
&.is-indeterminate::before { &.is-indeterminate::before {
content: "\f068"; content: '\f068';
} }
&.is-active::before { &.is-active::before {
content: "\f00c"; content: '\f00c';
} }
} }
} }
} }
.dropdown-title { .dropdown-title {
position: relative; position: relative;
padding: 2px 25px 10px; padding: 2px 25px 10px;
@ -724,7 +727,6 @@
} }
} }
.dropdown-menu-due-date { .dropdown-menu-due-date {
.dropdown-content { .dropdown-content {
max-height: 230px; max-height: 230px;
@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
.projects-list-frequent-container, .projects-list-frequent-container,
.projects-list-search-container, { .projects-list-search-container {
padding: 8px 0; padding: 8px 0;
overflow-y: auto; overflow-y: auto;
li.section-empty.section-failure {
color: $callout-danger-color;
}
} }
.section-header, .section-header,
@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size; font-size: $gl-font-size;
} }
.projects-list-frequent-container,
.projects-list-search-container {
li.section-empty.section-failure {
color: $callout-danger-color;
}
}
.search-input-container { .search-input-container {
position: relative; position: relative;
padding: 4px $gl-padding; padding: 4px $gl-padding;
@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
.projects-list-item-container { .projects-list-item-container {
.project-item-avatar-container .project-item-avatar-container .project-item-metadata-container {
.project-item-metadata-container {
float: left; float: left;
} }

View File

@ -40,10 +40,6 @@
.project-home-panel { .project-home-panel {
padding-left: 0 !important; padding-left: 0 !important;
.project-avatar {
display: block;
}
.project-repo-buttons, .project-repo-buttons,
.git-clone-holder { .git-clone-holder {
display: none; display: none;

View File

@ -468,6 +468,14 @@
margin-bottom: 10px; margin-bottom: 10px;
white-space: normal; white-space: normal;
.ci-job-dropdown-container {
// override dropdown.scss
.dropdown-menu li button {
padding: 0;
text-align: center;
}
}
// ensure .build-content has hover style when action-icon is hovered // ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content { .ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover; @extend .build-content:hover;

View File

@ -17,6 +17,7 @@
} }
.ide-view { .ide-view {
position: relative;
display: flex; display: flex;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
margin-top: 0; margin-top: 0;
@ -880,6 +881,26 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.ide-file-finder-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
}
.ide-file-finder {
top: 10px;
left: 50%;
transform: translateX(-50%);
.highlighted {
color: $blue-500;
font-weight: $gl-font-weight-bold;
}
}
.ide-commit-message-field { .ide-commit-message-field {
height: 200px; height: 200px;
background-color: $white-light; background-color: $white-light;

View File

@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
# Set @user for Devise views
@user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
return locked_user_redirect(user) unless user.can?(:log_in) return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id

View File

@ -165,8 +165,8 @@ module IssuableCollections
[:project, :author, :assignees, :labels, :milestone, project: :namespace] [:project, :author, :assignees, :labels, :milestone, project: :namespace]
when 'MergeRequest' when 'MergeRequest'
[ [
:source_project, :target_project, :author, :assignee, :labels, :milestone, :target_project, :author, :assignee, :labels, :milestone,
head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
] ]
end end
end end

View File

@ -0,0 +1,31 @@
class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
extend ::Gitlab::Utils::Override
def self.define_providers!
return unless Gitlab::Auth::LDAP::Config.enabled?
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
alias_method server['provider_name'], :ldap
end
end
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
sign_in_user_flow(Gitlab::Auth::LDAP::User)
end
define_providers!
override :set_remember_me
def set_remember_me(user)
user.remember_me = params[:remember_me] if user.persisted?
end
override :fail_login
def fail_login(user)
flash[:alert] = 'Access denied for your LDAP account.'
redirect_to new_user_session_path
end
end

View File

@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml, :cas3] protect_from_forgery except: [:kerberos, :saml, :cas3]
Gitlab.config.omniauth.providers.each do |provider| def handle_omniauth
define_method provider['name'] do omniauth_flow(Gitlab::Auth::OAuth)
handle_omniauth
end
end end
if Gitlab::Auth::LDAP::Config.enabled? Gitlab.config.omniauth.providers.each do |provider|
Gitlab::Auth::LDAP::Config.available_servers.each do |server| alias_method provider['name'], :handle_omniauth
define_method server['provider_name'] do
ldap
end
end
end end
# Extend the standard implementation to also increment # Extend the standard implementation to also increment
@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
error ||= exception.error if exception.respond_to?(:error) error ||= exception.error if exception.respond_to?(:error)
error ||= exception.message if exception.respond_to?(:message) error ||= exception.message if exception.respond_to?(:message)
error ||= env["omniauth.error.type"].to_s error ||= env["omniauth.error.type"].to_s
error.to_s.humanize if error error.to_s.humanize if error
end end
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
ldap_user.save if ldap_user.changed? # will also save new users
@user = ldap_user.gl_user
@user.remember_me = params[:remember_me] if ldap_user.persisted?
# Do additional LDAP checks for the user filter and EE features
if ldap_user.allowed?
if @user.two_factor_enabled?
prompt_for_two_factor(@user)
else
log_audit_event(@user, with: oauth['provider'])
sign_in_and_redirect(@user)
end
else
fail_ldap_login
end
end
def saml def saml
if current_user omniauth_flow(Gitlab::Auth::Saml)
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
else
redirect_to after_sign_in_path_for(current_user)
end
else
saml_user = Gitlab::Auth::Saml::User.new(oauth)
saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
end
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end end
def omniauth_error def omniauth_error
@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private private
def handle_omniauth def omniauth_flow(auth_module, identity_linker: nil)
if current_user if current_user
# Add new authentication method
current_user.identities
.with_extern_uid(oauth['provider'], oauth['uid'])
.first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider']) log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
oauth_user.save
@user = oauth_user.gl_user
continue_login_process identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
identity_linker.link
if identity_linker.changed?
redirect_identity_linked
elsif identity_linker.error_message.present?
redirect_identity_link_failed(identity_linker.error_message)
else
redirect_identity_exists
end
else
sign_in_user_flow(auth_module::User)
end end
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError end
handle_disabled_provider
rescue Gitlab::Auth::OAuth::User::SignupDisabledError def redirect_identity_exists
handle_signup_error redirect_to after_sign_in_path_for(current_user)
end
def redirect_identity_link_failed(error_message)
redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
end
def redirect_identity_linked
redirect_to profile_account_path, notice: 'Authentication method updated'
end end
def handle_service_ticket(provider, ticket) def handle_service_ticket(provider, ticket)
@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket session[:service_tickets][provider] = ticket
end end
def continue_login_process def sign_in_user_flow(auth_user_class)
# Only allow properly saved users to login. auth_user = auth_user_class.new(oauth)
if @user.persisted? && @user.valid? user = auth_user.find_and_update!
log_audit_event(@user, with: oauth['provider'])
if @user.two_factor_enabled? if auth_user.valid_sign_in?
params[:remember_me] = '1' if remember_me? log_audit_event(user, with: oauth['provider'])
prompt_for_two_factor(@user)
set_remember_me(user)
if user.two_factor_enabled?
prompt_for_two_factor(user)
else else
remember_me(@user) if remember_me? sign_in_and_redirect(user)
sign_in_and_redirect(@user)
end end
else else
fail_login fail_login(user)
end end
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end end
def handle_signup_error def handle_signup_error
@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@oauth ||= request.env['omniauth.auth'] @oauth ||= request.env['omniauth.auth']
end end
def fail_login def fail_login(user)
error_message = @user.errors.full_messages.to_sentence error_message = user.errors.full_messages.to_sentence
return redirect_to omniauth_error_path(oauth['provider'], error: error_message) return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end end
def fail_ldap_login
flash[:alert] = 'Access denied for your LDAP account.'
redirect_to new_user_session_path
end
def fail_auth0_login def fail_auth0_login
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.' flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
.for_authentication.security_event .for_authentication.security_event
end end
def set_remember_me(user)
return unless remember_me?
if user.two_factor_enabled?
params[:remember_me] = '1'
else
remember_me(user)
end
end
def remember_me? def remember_me?
request_params = request.env['omniauth.params'] request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present? (request_params['remember_me'] == '1') if request_params.present?

View File

@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users) users = by_active(users)
users = by_external_identity(users) users = by_external_identity(users)
users = by_external(users) users = by_external(users)
users = by_2fa(users)
users = by_created_at(users) users = by_created_at(users)
users = by_custom_attributes(users) users = by_custom_attributes(users)
@ -76,4 +77,15 @@ class UsersFinder
users.external users.external
end end
def by_2fa(users)
case params[:two_factor]
when 'enabled'
users.with_two_factor
when 'disabled'
users.without_two_factor
else
users
end
end
end end

View File

@ -81,6 +81,14 @@ module GitlabRoutingHelper
end end
end end
def edit_milestone_path(entity, *args)
if entity.parent.is_a?(Group)
edit_group_milestone_path(entity.parent, entity, *args)
else
edit_project_milestone_path(entity.parent, entity, *args)
end
end
def toggle_subscription_path(entity, *args) def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue) if entity.is_a?(Issue)
toggle_subscription_project_issue_path(entity.project, entity) toggle_subscription_project_issue_path(entity.project, entity)

View File

@ -27,6 +27,7 @@ module Ci
has_one :metadata, class_name: 'Ci::BuildMetadata' has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :gitlab_deploy_token, to: :project
## ##
# The "environment" field for builds is a String, and is the unexpanded name! # The "environment" field for builds is a String, and is the unexpanded name!
@ -604,6 +605,7 @@ module Ci
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER) .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false) .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false) .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
.concat(deploy_token_variables)
end end
end end
@ -654,6 +656,15 @@ module Ci
end end
end end
def deploy_token_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless gitlab_deploy_token
variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name)
variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
end
end
def environment_url def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url options&.dig(:environment, :url) || persisted_environment&.external_url
end end

View File

@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present? use_asset_host = asset_host.present?
use_authentication = respond_to?(:public?) && !public?
# Avatars for private and internal groups and projects require authentication to be viewed, # Avatars for private and internal groups and projects require authentication to be viewed,
# which means they can only be served by Rails, on the regular GitLab host. # which means they can only be served by Rails, on the regular GitLab host.
# If an asset host is configured, we need to return the fully qualified URL # If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host. # instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
if use_asset_host && respond_to?(:public?) && !public? if use_asset_host && use_authentication
use_asset_host = false use_asset_host = false
only_path = false only_path = false
end end
@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root url_base << gitlab_config.relative_url_root
end end
url_base + avatar.url url_base + avatar.local_url
end end
end end

View File

@ -31,7 +31,7 @@ module ProtectedRef
end end
end end
def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil) def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user) access_level.check_access(user)
end end

View File

@ -102,7 +102,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances, # the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases. # we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active? return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path RequestStore[full_path_key] ||= uncached_full_path
end end
@ -124,6 +124,11 @@ module Routable
end end
end end
# Group would override this to check from association
def owned_by?(user)
owner == user
end
private private
def set_path_errors def set_path_errors

View File

@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
add_authentication_token_field :token add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
default_value_for(:expires_at) { Forever.date } default_value_for(:expires_at) { Forever.date }
@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
scope :active, -> { where("revoked = false AND expires_at >= NOW()") } scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
def self.gitlab_deploy_token
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
end
def revoke! def revoke!
update!(revoked: true) update!(revoked: true)
end end

View File

@ -125,6 +125,10 @@ class Group < Namespace
self[:lfs_enabled] self[:lfs_enabled]
end end
def owned_by?(user)
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users( GroupMember.add_users(
self, self,

View File

@ -68,6 +68,11 @@ class Project < ActiveRecord::Base
after_save :update_project_statistics, if: :namespace_id_changed? after_save :update_project_statistics, if: :namespace_id_changed?
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
after_create :set_last_activity_at after_create :set_last_activity_at
after_create :set_last_repository_updated_at after_create :set_last_repository_updated_at
after_update :update_forks_visibility_level after_update :update_forks_visibility_level
@ -231,6 +236,7 @@ class Project < ActiveRecord::Base
has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge' has_many :project_badges, class_name: 'ProjectBadge'
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
@ -1041,13 +1047,6 @@ class Project < ActiveRecord::Base
"#{web_url}.git" "#{web_url}.git"
end end
def user_can_push_to_empty_repo?(user)
return false unless empty_repo?
return false unless Ability.allowed?(user, :push_code, self)
!ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
def forked? def forked?
return true if fork_network && fork_network.root_project != self return true if fork_network && fork_network.root_project != self
@ -1879,6 +1878,10 @@ class Project < ActiveRecord::Base
[] []
end end
def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
private private
def storage def storage
@ -2004,10 +2007,11 @@ class Project < ActiveRecord::Base
def fetch_branch_allows_maintainer_push?(user, branch_name) def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do check_access = -> do
next false if empty_repo?
merge_request = source_of_merge_requests.opened merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true) .where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name) .find_by(source_branch: branch_name)
merge_request&.can_be_merged_by?(user) merge_request&.can_be_merged_by?(user)
end end

View File

@ -0,0 +1,16 @@
class ProjectCiCdSetting < ActiveRecord::Base
belongs_to :project
# The version of the schema that first introduced this model/table.
MINIMUM_SCHEMA_VERSION = 20180403035759
def self.available?
@available ||=
ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
end
def self.reset_column_information
@available = nil
super
end
end

View File

@ -1,5 +1,51 @@
require "flowdock-git-hook" require "flowdock-git-hook"
# Flow dock depends on Grit to compute the number of commits between two given
# commits. To make this depend on Gitaly, a monkey patch is applied
module Flowdock
class Git
# pass down a Repository all the way down
def repo
@options[:repo]
end
def config
{}
end
def messages
Git::Builder.new(repo: repo,
ref: @ref,
before: @from,
after: @to,
commit_url: @commit_url,
branch_url: @branch_url,
diff_url: @diff_url,
repo_url: @repo_url,
repo_name: @repo_name,
permanent_refs: @permanent_refs,
tags: tags
).to_hashes
end
class Builder
def commits
@repo.commits_between(@before, @after).map do |commit|
{
url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
id: commit.sha,
message: commit.message,
author: {
name: commit.author_name,
email: commit.author_email
}
}
end
end
end
end
end
class FlowdockService < Service class FlowdockService < Service
prop_accessor :token prop_accessor :token
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before], data[:before],
data[:after], data[:after],
token: token, token: token,
repo: project.repository.path_to_repo, repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s", commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"

View File

@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
# Masters, owners and admins are allowed to create the default branch
if default_branch_protected? && project.empty_repo?
return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
super
end
# Check if branch name is marked as protected in the system # Check if branch name is marked as protected in the system
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected? return true if project.empty_repo? && default_branch_protected?

View File

@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) } condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
end end
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
end end
rule { admin } .enable :read_group rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
rule { has_projects }.policy do
enable :read_group
enable :read_label
end
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace

View File

@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper include GitlabRoutingHelper
include StorageHelper include StorageHelper
include TreeHelper include TreeHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
presents :project presents :project
@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def can_current_user_push_to_branch?(branch) def can_current_user_push_to_branch?(branch)
return false unless repository.branch_exists?(branch) user_access(project).can_push_to_branch?(branch)
end
::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) def can_current_user_push_to_default_branch?
can_current_user_push_to_branch?(default_branch)
end end
def files_anchor_data def files_anchor_data
@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def new_file_anchor_data def new_file_anchor_data
if current_user && can_current_user_push_code? if current_user && can_current_user_push_to_default_branch?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('New file'), label: _('New file'),
link: project_new_blob_path(project, default_branch || 'master'), link: project_new_blob_path(project, default_branch || 'master'),
@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def readme_anchor_data def readme_anchor_data
if current_user && can_current_user_push_code? && repository.readme.blank? if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add Readme'), label: _('Add Readme'),
link: add_readme_path) link: add_readme_path)
@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def changelog_anchor_data def changelog_anchor_data
if current_user && can_current_user_push_code? && repository.changelog.blank? if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add Changelog'), label: _('Add Changelog'),
link: add_changelog_path) link: add_changelog_path)
@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def license_anchor_data def license_anchor_data
if current_user && can_current_user_push_code? && repository.license_blob.blank? if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add License'), label: _('Add License'),
link: add_license_path) link: add_license_path)
@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def contribution_guide_anchor_data def contribution_guide_anchor_data
if current_user && can_current_user_push_code? && repository.contribution_guide.blank? if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add Contribution guide'), label: _('Add Contribution guide'),
link: add_contribution_guide_path) link: add_contribution_guide_path)
@ -260,7 +263,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?, OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled? elsif auto_devops_enabled?
OpenStruct.new(enabled: true, OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'), label: _('Auto DevOps enabled'),

View File

@ -138,8 +138,10 @@ module QuickActions
'Remove assignee' 'Remove assignee'
end end
end end
explanation do explanation do |users = nil|
"Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}." assignees = issuable.assignees
assignees &= users if users.present? && issuable.allows_multiple_assignees?
"Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
end end
params do params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : '' issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
@ -268,6 +270,26 @@ module QuickActions
end end
end end
desc 'Copy labels and milestone from other issue or merge request'
explanation do |source_issuable|
"Copy labels and milestone from #{source_issuable.to_reference}."
end
params '#issue | !merge_request'
condition do
issuable.persisted? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
extract_references(issuable_param, :merge_request).first
end
command :copy_metadata do |source_issuable|
if source_issuable.present? && source_issuable.project.id == issuable.project.id
@updates[:add_label_ids] = source_issuable.labels.map(&:id)
@updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
end
end
desc 'Add a todo' desc 'Add a todo'
explanation 'Adds a todo.' explanation 'Adds a todo.'
condition do condition do

View File

@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model !!model
end end
def local_url
File.join('/', self.class.base_dir, dynamic_segment, filename)
end
private private
# Designed to be overridden by child uploaders that have a dynamic path # Designed to be overridden by child uploaders that have a dynamic path

View File

@ -21,7 +21,7 @@
.help-block .help-block
Manage repository storage paths. Learn more in the Manage repository storage paths. Learn more in the
= succeed "." do = succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages") = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section .sub-section
%h4 Circuit breaker %h4 Circuit breaker
.form-group .form-group

View File

@ -22,7 +22,7 @@
%hr %hr
%p %p
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
@ -58,7 +58,9 @@
touch README.md touch README.md
git add README.md git add README.md
git commit -m "add README" git commit -m "add README"
git push -u origin master - if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin master
%fieldset %fieldset
%h5 Existing folder %h5 Existing folder
@ -69,7 +71,9 @@
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add . git add .
git commit -m "Initial commit" git commit -m "Initial commit"
git push -u origin master - if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin master
%fieldset %fieldset
%h5 Existing Git repository %h5 Existing Git repository
@ -78,8 +82,10 @@
cd existing_repo cd existing_repo
git remote rename origin old-origin git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git push -u origin --all - if @project.can_current_user_push_to_default_branch?
git push -u origin --tags %span><
git push -u origin --all
git push -u origin --tags
- if can? current_user, :remove_project, @project - if can? current_user, :remove_project, @project
.prepend-top-20 .prepend-top-20

View File

@ -15,7 +15,7 @@
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to project_ci_lint_path(@project), class: 'btn btn-default' do
%span CI lint %span CI lint
.content-list.builds-content-list .content-list.builds-content-list

View File

@ -26,7 +26,7 @@
%ul %ul
- pipeline.yaml_errors.split(",").each do |error| - pipeline.yaml_errors.split(",").each do |error|
%li= error %li= error
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning

View File

@ -29,7 +29,7 @@
docker login #{Gitlab.config.registry.host_port} docker login #{Gitlab.config.registry.host_port}
%br %br
%p %p
- deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br %br
%p %p

View File

@ -0,0 +1,40 @@
.row.prepend-top-default
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong= s_('CICD|Enable Auto DevOps')
%br
= s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong= s_('CICD|Disable Auto DevOps')
%br
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
%br
= s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
= form.label :domain, class:"prepend-top-10" do
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"

View File

@ -3,44 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project) = form_errors(@project)
%fieldset.builds-feature %fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
.form-group.append-bottom-default.js-secret-runner-token .form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light' = f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder .form-control.js-secret-value-placeholder

View File

@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout or Auto DevOps. Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content .settings-content
= render 'form' = render 'form'
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps (Beta)')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
= render 'autodevops_form'
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4

View File

@ -9,7 +9,7 @@
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.banner-buttons .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' } 'aria-label' => 'Dismiss Auto DevOps box' }

View File

@ -0,0 +1,5 @@
---
title: Introduce new ProjectCiCdSetting model with group_runners_enabled
merge_request: 18144
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request:
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Add an API endpoint to download git repository snapshots
merge_request: 18173
author:
type: added

View File

@ -0,0 +1,6 @@
---
title: Fix discussions API setting created_at for notable in a group or notable in
a project in a group with owners
merge_request: 18464
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Reduce queries on merge requests list page for merge requests from forks
merge_request: 18561
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Create settings section for autodevops
merge_request: 18321
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Expose Deploy Token data as environment varialbes on CI/CD jobs
merge_request: 18414
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fixed wrong avatar URL when the avatar is on object storage.
merge_request: 18092
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Fix `Trace::HttpIO` can not render multi-byte chars
merge_request: 18417
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Align action icons in pipeline graph
merge_request:
author:
type: fixed

View File

@ -1,6 +0,0 @@
---
title: '[API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors`
when no value is passed for `order_by` or `sort`'
merge_request: 18393
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Add index to file_store on ci_job_artifacts
merge_request: 18444
author:
type: performance

View File

@ -1,6 +0,0 @@
---
title: Fix specifying a non-default ref when requesting an archive using the legacy
URL
merge_request: 18468
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix project creation for user endpoint when jobs_enabled parameter supplied
merge_request:
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Removes 'No Job log' message from build trace
merge_request: 18523
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update links to /ci/lint with ones to project ci/lint
merge_request: 18539
author: Takuya Noguchi
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix unassign slash command preview
merge_request: 18447
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Validate project path prior to hitting the database.
merge_request: 18322
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Add Copy metadata quick action
merge_request: 16473
author: Mateusz Bajorski
type: added

View File

@ -0,0 +1,5 @@
---
title: Align project avatar on small viewports
merge_request: 18513
author: George Tsiolis
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add missing changelog type to docs
merge_request: 18526
author: "@blackst0ne"
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix errors on pushing to an empty repository
merge_request: 18462
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add 2FA filter to users API for admins only
merge_request: 18503
author:
type: changed

Some files were not shown because too many files have changed in this diff Show More