Skip to content
Snippets Groups Projects
Commit f03a645e authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 0dea53d5
No related branches found
No related tags found
No related merge requests found
Showing
with 553 additions and 44 deletions
Loading
Loading
@@ -42,12 +42,19 @@ export default {
return {
showDetail: false,
detailIssue: boardsStore.detail,
multiSelect: boardsStore.multiSelect,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
multiSelectVisible() {
return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
},
canMultiSelect() {
return gon.features && gon.features.multiSelectBoard;
},
},
methods: {
mouseDown() {
Loading
Loading
@@ -58,14 +65,20 @@ export default {
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
 
// If CMD or CTRL is clicked
const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue');
eventHub.$emit('clearDetailIssue', isMultiSelect);
if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
}
} else {
eventHub.$emit('newDetailIssue', this.issue);
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
boardsStore.setListDetail(this.list);
}
}
Loading
Loading
@@ -77,6 +90,7 @@ export default {
<template>
<li
:class="{
'multi-select': multiSelectVisible,
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
'is-active': issueDetailVisible,
Loading
Loading
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import Sortable from 'sortablejs';
import { Sortable, MultiDrag } from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui';
import _ from 'underscore';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableStart } from '../mixins/sortable_default_options';
import { sprintf, __ } from '~/locale';
import createFlash from '~/flash';
import {
getBoardSortableDefaultOptions,
sortableStart,
sortableEnd,
} from '../mixins/sortable_default_options';
if (gon.features && gon.features.multiSelectBoard) {
Sortable.mount(new MultiDrag());
}
 
export default {
name: 'BoardList',
Loading
Loading
@@ -54,6 +64,14 @@ export default {
showIssueForm: false,
};
},
computed: {
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} issues'), {
pageSize: this.list.issues.length,
total: this.list.issuesSize,
});
},
},
watch: {
filters: {
handler() {
Loading
Loading
@@ -87,11 +105,20 @@ export default {
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
},
mounted() {
const multiSelectOpts = {};
if (gon.features && gon.features.multiSelectBoard) {
multiSelectOpts.multiDrag = true;
multiSelectOpts.selectedClass = 'js-multi-select';
multiSelectOpts.animation = 500;
}
const options = getBoardSortableDefaultOptions({
scroll: true,
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
removeCloneOnHide: false,
...multiSelectOpts,
group: {
name: 'issues',
/**
Loading
Loading
@@ -145,25 +172,66 @@ export default {
card.showDetail = false;
 
const { list } = card;
const issue = list.findIssue(Number(e.item.dataset.issueId));
boardsStore.startMoving(list, issue);
 
sortableStart();
},
onAdd: e => {
boardsStore.moveIssueToList(
boardsStore.moving.list,
this.list,
boardsStore.moving.issue,
e.newIndex,
);
const { items = [], newIndicies = [] } = e;
if (items.length) {
// Not using e.newIndex here instead taking a min of all
// the newIndicies. Basically we have to find that during
// a drop what is the index we're going to start putting
// all the dropped elements from.
const newIndex = Math.min(...newIndicies.map(obj => obj.index).filter(i => i !== -1));
const issues = items.map(item =>
boardsStore.moving.list.findIssue(Number(item.dataset.issueId)),
);
 
this.$nextTick(() => {
e.item.remove();
});
boardsStore.moveMultipleIssuesToList({
listFrom: boardsStore.moving.list,
listTo: this.list,
issues,
newIndex,
});
} else {
boardsStore.moveIssueToList(
boardsStore.moving.list,
this.list,
boardsStore.moving.issue,
e.newIndex,
);
this.$nextTick(() => {
e.item.remove();
});
}
},
onUpdate: e => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
const { items = [], newIndicies = [], oldIndicies = [] } = e;
if (items.length) {
const newIndex = Math.min(...newIndicies.map(obj => obj.index));
const issues = items.map(item =>
boardsStore.moving.list.findIssue(Number(item.dataset.issueId)),
);
boardsStore.moveMultipleIssuesInList({
list: this.list,
issues,
oldIndicies: oldIndicies.map(obj => obj.index),
newIndex,
idArray: sortedArray,
});
e.items.forEach(el => {
Sortable.utils.deselect(el);
});
boardsStore.clearMultiSelect();
return;
}
boardsStore.moveIssueInList(
this.list,
boardsStore.moving.issue,
Loading
Loading
@@ -172,9 +240,133 @@ export default {
sortedArray,
);
},
onEnd: e => {
const { items = [], clones = [], to } = e;
// This is not a multi select operation
if (!items.length && !clones.length) {
sortableEnd();
return;
}
let toList;
if (to) {
const containerEl = to.closest('.js-board-list');
toList = boardsStore.findList('id', Number(containerEl.dataset.board));
}
/**
* onEnd is called irrespective if the cards were moved in the
* same list or the other list. Don't remove items if it's same list.
*/
const isSameList = toList && toList.id === this.list.id;
if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) {
const issues = items.map(item => this.list.findIssue(Number(item.dataset.issueId)));
if (_.compact(issues).length && !boardsStore.issuesAreContiguous(this.list, issues)) {
const indexes = [];
const ids = this.list.issues.map(i => i.id);
issues.forEach(issue => {
const index = ids.indexOf(issue.id);
if (index > -1) {
indexes.push(index);
}
});
// Descending sort because splice would cause index discrepancy otherwise
const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1));
sortedIndexes.forEach(i => {
/**
* **setTimeout and splice each element one-by-one in a loop
* is intended.**
*
* The problem here is all the indexes are in the list but are
* non-contiguous. Due to that, when we splice all the indexes,
* at once, Vue -- during a re-render -- is unable to find reference
* nodes and the entire app crashes.
*
* If the indexes are contiguous, this piece of code is not
* executed. If it is, this is a possible regression. Only when
* issue indexes are far apart, this logic should ever kick in.
*/
setTimeout(() => {
this.list.issues.splice(i, 1);
}, 0);
});
}
}
if (!toList) {
createFlash(__('Something went wrong while performing the action.'));
}
if (!isSameList) {
boardsStore.clearMultiSelect();
// Since Vue's list does not re-render the same keyed item, we'll
// remove `multi-select` class to express it's unselected
if (clones && clones.length) {
clones.forEach(el => el.classList.remove('multi-select'));
}
// Due to some bug which I am unable to figure out
// Sortable does not deselect some pending items from the
// source list.
// We'll just do it forcefully here.
Array.from(document.querySelectorAll('.js-multi-select') || []).forEach(item => {
Sortable.utils.deselect(item);
});
/**
* SortableJS leaves all the moving items "as is" on the DOM.
* Vue picks up and rehydrates the DOM, but we need to explicity
* remove the "trash" items from the DOM.
*
* This is in parity to the logic on single item move from a list/in
* a list. For reference, look at the implementation of onAdd method.
*/
this.$nextTick(() => {
if (items && items.length) {
items.forEach(item => {
item.remove();
});
}
});
}
sortableEnd();
},
onMove(e) {
return !e.related.classList.contains('board-list-count');
},
onSelect(e) {
const {
item: { classList },
} = e;
if (
classList &&
classList.contains('js-multi-select') &&
!classList.contains('multi-select')
) {
Sortable.utils.deselect(e.item);
}
},
onDeselect: e => {
const {
item: { dataset, classList },
} = e;
if (
classList &&
classList.contains('multi-select') &&
!classList.contains('js-multi-select')
) {
const issue = this.list.findIssue(Number(dataset.issueId));
boardsStore.toggleMultiSelect(issue);
}
},
});
 
this.sortable = Sortable.create(this.$refs.list, options);
Loading
Loading
@@ -260,7 +452,7 @@ export default {
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
<gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
<span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
</ul>
</div>
Loading
Loading
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
export default {
ListType,
};
Loading
Loading
@@ -146,7 +146,7 @@ export default () => {
updateTokens() {
this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
updateDetailIssue(newIssue, multiSelect = false) {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
Loading
Loading
@@ -185,9 +185,23 @@ export default () => {
});
}
 
if (multiSelect) {
boardsStore.toggleMultiSelect(newIssue);
if (boardsStore.detail.issue) {
boardsStore.clearDetailIssue();
return;
}
return;
}
boardsStore.setIssueDetail(newIssue);
},
clearDetailIssue() {
clearDetailIssue(multiSelect = false) {
if (multiSelect) {
boardsStore.clearMultiSelect();
}
boardsStore.clearDetailIssue();
},
toggleSubscription(id) {
Loading
Loading
Loading
Loading
@@ -5,6 +5,7 @@ import ListLabel from './label';
import ListAssignee from './assignee';
import ListIssue from 'ee_else_ce/boards/models/issue';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import flash from '~/flash';
import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone';
 
Loading
Loading
@@ -176,6 +177,53 @@ class List {
});
}
 
addMultipleIssues(issues, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
const listHasIssues = issues.every(issue => this.findIssue(issue.id));
if (!listHasIssues) {
if (newIndex !== undefined) {
if (this.issues[newIndex - 1]) {
moveBeforeId = this.issues[newIndex - 1].id;
}
if (this.issues[newIndex]) {
moveAfterId = this.issues[newIndex].id;
}
this.issues.splice(newIndex, 0, ...issues);
} else {
this.issues.push(...issues);
}
if (this.label) {
issues.forEach(issue => issue.addLabel(this.label));
}
if (this.assignee) {
if (listFrom && listFrom.type === 'assignee') {
issues.forEach(issue => issue.removeAssignee(listFrom.assignee));
}
issues.forEach(issue => issue.addAssignee(this.assignee));
}
if (IS_EE && this.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issues.forEach(issue => issue.removeMilestone(listFrom.milestone));
}
issues.forEach(issue => issue.addMilestone(this.milestone));
}
if (listFrom) {
this.issuesSize += issues.length;
this.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
}
}
}
addIssue(issue, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
Loading
Loading
@@ -230,6 +278,23 @@ class List {
});
}
 
moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
oldIndicies.reverse().forEach(index => {
this.issues.splice(index, 1);
});
this.issues.splice(newIndex, 0, ...issues);
gl.boardService
.moveMultipleIssues({
ids: issues.map(issue => issue.id),
fromListId: null,
toListId: null,
moveBeforeId,
moveAfterId,
})
.catch(() => flash(__('Something went wrong while moving issues.')));
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
gl.boardService
.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
Loading
Loading
@@ -238,10 +303,37 @@ class List {
});
}
 
updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) {
gl.boardService
.moveMultipleIssues({
ids: issues.map(issue => issue.id),
fromListId: listFrom.id,
toListId: this.id,
moveBeforeId,
moveAfterId,
})
.catch(() => flash(__('Something went wrong while moving issues.')));
}
findIssue(id) {
return this.issues.find(issue => issue.id === id);
}
 
removeMultipleIssues(removeIssues) {
const ids = removeIssues.map(issue => issue.id);
this.issues = this.issues.filter(issue => {
const matchesRemove = ids.includes(issue.id);
if (matchesRemove) {
this.issuesSize -= 1;
issue.removeLabel(this.label);
}
return !matchesRemove;
});
}
removeIssue(removeIssue) {
this.issues = this.issues.filter(issue => {
const matchesRemove = removeIssue.id === issue.id;
Loading
Loading
Loading
Loading
@@ -48,6 +48,16 @@ export default class BoardService {
return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
}
 
moveMultipleIssues({
ids,
fromListId = null,
toListId = null,
moveBeforeId = null,
moveAfterId = null,
}) {
return boardsStore.moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId });
}
newIssue(id, issue) {
return boardsStore.newIssue(id, issue);
}
Loading
Loading
Loading
Loading
@@ -11,6 +11,7 @@ import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub';
import { ListType } from '../constants';
 
const boardsStore = {
disabled: false,
Loading
Loading
@@ -39,6 +40,7 @@ const boardsStore = {
issue: {},
list: {},
},
multiSelect: { list: [] },
 
setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
Loading
Loading
@@ -51,7 +53,6 @@ const boardsStore = {
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
};
},
create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
Loading
Loading
@@ -134,6 +135,107 @@ const boardsStore = {
Object.assign(this.moving, { list, issue });
},
 
moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
const issueTo = issues.map(issue => listTo.findIssue(issue.id));
const issueLists = _.flatten(issues.map(issue => issue.getLists()));
const listLabels = issueLists.map(list => list.label);
const hasMoveableIssues = _.compact(issueTo).length > 0;
if (!hasMoveableIssues) {
// Check if target list assignee is already present in this issue
if (
listTo.type === ListType.assignee &&
listFrom.type === ListType.assignee &&
issues.some(issue => issue.findAssignee(listTo.assignee))
) {
const targetIssues = issues.map(issue => listTo.findIssue(issue.id));
targetIssues.forEach(targetIssue => targetIssue.removeAssignee(listFrom.assignee));
} else if (listTo.type === 'milestone') {
const currentMilestones = issues.map(issue => issue.milestone);
const currentLists = this.state.lists
.filter(list => list.type === 'milestone' && list.id !== listTo.id)
.filter(list =>
list.issues.some(listIssue => issues.some(issue => listIssue.id === issue.id)),
);
issues.forEach(issue => {
currentMilestones.forEach(milestone => {
issue.removeMilestone(milestone);
});
});
issues.forEach(issue => {
issue.addMilestone(listTo.milestone);
});
currentLists.forEach(currentList => {
issues.forEach(issue => {
currentList.removeIssue(issue);
});
});
listTo.addMultipleIssues(issues, listFrom, newIndex);
} else {
// Add to new lists issues if it doesn't already exist
listTo.addMultipleIssues(issues, listFrom, newIndex);
}
} else {
listTo.updateMultipleIssues(issues, listFrom);
issues.forEach(issue => {
issue.removeLabel(listFrom.label);
});
}
if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) {
issueLists.forEach(list => {
issues.forEach(issue => {
list.removeIssue(issue);
});
});
issues.forEach(issue => {
issue.removeLabels(listLabels);
});
} else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) {
issues.forEach(issue => {
issue.removeAssignee(listFrom.assignee);
});
issueLists.forEach(list => {
issues.forEach(issue => {
list.removeIssue(issue);
});
});
} else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) {
issues.forEach(issue => {
issue.removeMilestone(listFrom.milestone);
});
issueLists.forEach(list => {
issues.forEach(issue => {
list.removeIssue(issue);
});
});
} else if (
this.shouldRemoveIssue(listFrom, listTo) &&
this.issuesAreContiguous(listFrom, issues)
) {
listFrom.removeMultipleIssues(issues);
}
},
issuesAreContiguous(list, issues) {
// When there's only 1 issue selected, we can return early.
if (issues.length === 1) return true;
// Create list of ids for issues involved.
const listIssueIds = list.issues.map(issue => issue.id);
const movedIssueIds = issues.map(issue => issue.id);
// Check if moved issue IDs is sub-array
// of source list issue IDs (i.e. contiguous selection).
return listIssueIds.join('|').includes(movedIssueIds.join('|'));
},
moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
Loading
Loading
@@ -195,6 +297,17 @@ const boardsStore = {
 
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + issues.length], 10) || null;
list.moveMultipleIssues({
issues,
oldIndicies,
newIndex,
moveBeforeId: beforeId,
moveAfterId: afterId,
});
},
findList(key, val, type = 'label') {
const filteredList = this.state.lists.filter(list => {
const byType = type
Loading
Loading
@@ -260,6 +373,10 @@ const boardsStore = {
}`;
},
 
generateMultiDragPath(boardId) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`;
},
all() {
return axios.get(this.state.endpoints.listsEndpoint);
},
Loading
Loading
@@ -309,6 +426,16 @@ const boardsStore = {
});
},
 
moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) {
return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
ids,
});
},
newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), {
issue,
Loading
Loading
@@ -379,6 +506,25 @@ const boardsStore = {
setCurrentBoard(board) {
this.state.currentBoard = board;
},
toggleMultiSelect(issue) {
const selectedIssueIds = this.multiSelect.list.map(issue => issue.id);
const index = selectedIssueIds.indexOf(issue.id);
if (index === -1) {
this.multiSelect.list.push(issue);
return;
}
this.multiSelect.list = [
...this.multiSelect.list.slice(0, index),
...this.multiSelect.list.slice(index + 1),
];
},
clearMultiSelect() {
this.multiSelect.list = [];
},
};
 
BoardsStoreEE.initEESpecific(boardsStore);
Loading
Loading
import 'core-js/es/map';
import 'core-js/es/set';
import { Sortable } from 'sortablejs';
import simulateDrag from './simulate_drag';
import simulateInput from './simulate_input';
 
// Export to global space for rspec to use
window.simulateDrag = simulateDrag;
window.simulateInput = simulateInput;
window.Sortable = Sortable;
Loading
Loading
@@ -245,6 +245,7 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow;
line-height: $gl-padding;
list-style: none;
position: relative;
 
&:not(:last-child) {
margin-bottom: $gl-padding-8;
Loading
Loading
@@ -255,6 +256,11 @@
background-color: $blue-50;
}
 
&.multi-select {
border-color: $blue-200;
background-color: $blue-50;
}
.badge {
border: 0;
outline: 0;
Loading
Loading
Loading
Loading
@@ -5,6 +5,9 @@ class Groups::BoardsController < Groups::ApplicationController
include RecordUserLastActivity
 
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board)
end
 
private
 
Loading
Loading
Loading
Loading
@@ -7,6 +7,9 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board)
end
 
private
 
Loading
Loading
Loading
Loading
@@ -2258,6 +2258,16 @@ class Project < ApplicationRecord
setting
end
 
def drop_visibility_level!
if group && group.visibility_level < visibility_level
self.visibility_level = group.visibility_level
end
if Gitlab::CurrentSettings.restricted_visibility_levels.include?(visibility_level)
self.visibility_level = Gitlab::VisibilityLevel::PRIVATE
end
end
private
 
def closest_namespace_setting(name)
Loading
Loading
Loading
Loading
@@ -64,7 +64,7 @@ class JiraService < IssueTrackerService
url = URI.parse(client_url)
 
{
username: username,
username: username&.strip,
password: password,
site: URI.join(url, '/').to_s, # Intended to find the root
context_path: url.path,
Loading
Loading
---
title: 'JIRA Service: Improve username/email validation'
merge_request: 18397
author:
type: fixed
Loading
Loading
@@ -26,10 +26,11 @@ on those issues. Please select someone with relevant experience from the
If there is nobody mentioned with that expertise look in the commit history for
the affected files to find someone.
 
We also use [GitLab Triage](https://gitlab.com/gitlab-org/gitlab-triage) to
automate some triaging policies. This is currently set up as a
[scheduled pipeline](https://gitlab.com/gitlab-org/quality/triage-ops/pipeline_schedules/10512/edit)
running on [quality/triage-ops](https://gitlab.com/gitlab-org/quality/triage-ops) project.
We also use [GitLab Triage](https://gitlab.com/gitlab-org/gitlab-triage) to automate
some triaging policies. This is currently set up as a scheduled pipeline
(`https://gitlab.com/gitlab-org/quality/triage-ops/pipeline_schedules/10512/editpipeline_schedules/10512/edit`,
must have at least developer access to the project) running on [quality/triage-ops](https://gitlab.com/gitlab-org/quality/triage-ops)
project.
 
## Labels
 
Loading
Loading
Loading
Loading
@@ -81,7 +81,7 @@ You can improve the existing built-in templates or contribute new ones in the
#### Custom project templates **(PREMIUM)**
 
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/6860) in
[GitLab Premium](https://about.gitlab.com/pricing) 11.2.
[GitLab Premium](https://about.gitlab.com/pricing/) 11.2.
 
Creating new projects based on custom project templates is a convenient option for
quickly starting projects.
Loading
Loading
Loading
Loading
@@ -13,9 +13,9 @@ make sure that you have created and/or signed into an account on GitLab.
Depending on your operating system, you will need to use a shell of your preference.
Here are some suggestions:
 
- [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on macOS
- [Terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on macOS
- [GitBash](https://msysgit.github.io) on Windows
- [Linux Terminal](http://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux
- [Linux Terminal](https://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux
 
## Check if Git has already been installed
 
Loading
Loading
Loading
Loading
@@ -38,7 +38,7 @@ through the macOS App Store.
 
### Installing Homebrew
 
Once Xcode is installed browse to the [Homebrew website](http://brew.sh/index.html)
Once Xcode is installed browse to the [Homebrew website](https://brew.sh/index.html)
for the official Homebrew installation instructions.
 
### Installing Git via Homebrew
Loading
Loading
Loading
Loading
@@ -26,7 +26,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.1. Version Control and Git
 
1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
1. [Katakoda: Learn Git Version Control using Interactive Browser-Based Scenarios](https://www.katacoda.com/courses/git)
 
### 1.2. GitLab Basics
 
Loading
Loading
@@ -34,12 +34,12 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
1. [GitLab Basics - Article](../gitlab-basics/README.md)
1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2475-part-233-2/)
1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
 
### 1.3. Your GitLab Account
 
1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2434-create-an-account-on-gitlab/)
1. [Create and Add your SSH key to GitLab - Video](https://www.youtube.com/watch?v=54mxyLo3Mqk)
 
### 1.4. GitLab Projects
Loading
Loading
@@ -59,7 +59,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
### 1.6. The GitLab team
 
1. [About GitLab](https://about.gitlab.com/about/)
1. [About GitLab](https://about.gitlab.com/company/)
1. [GitLab Direction](https://about.gitlab.com/direction/)
1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
Loading
Loading
@@ -70,7 +70,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
### 1.7 Community and Support
 
1. [Getting Help](https://about.gitlab.com/getting-help/)
1. [Getting Help](https://about.gitlab.com/get-help/)
- Proposing Features and Reporting and Tracking bugs for GitLab
- The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
- Getting Technical Support
Loading
Loading
@@ -107,7 +107,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 2.3. Continuous Integration
 
1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
1. [GitLab CI - Product Page](https://about.gitlab.com/gitlab-ci/)
1. [GitLab CI - Product Page](https://about.gitlab.com/product/continuous-integration/)
1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
Loading
Loading
@@ -120,7 +120,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/devops/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
1. See **[Integrations](#39-integrations)** for integrations with other CI services.
 
### 2.4. Workflow
Loading
Loading
@@ -133,11 +133,11 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
### 2.5. GitLab Comparisons
 
1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
1. [GitLab Compared to Other Tools](https://about.gitlab.com/devops-tools/)
1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
1. [GitLab Compared to Atlassian (Recording 2016-03-03)](https://youtu.be/Nbzp1t45ERo)
1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq/)
1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/blog/2015/11/25/gitlab-review)
 
## 3. GitLab Advanced
 
Loading
Loading
@@ -145,13 +145,13 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/white-paper/2016-state-of-devops-report)
1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/whitepaper/2016-state-of-devops-report)
 
### 3.2. Installing GitLab with Omnibus
 
1. [What is Omnibus - Video](https://www.youtube.com/watch?v=XTmpKudd-Oo)
1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-3/material/)
1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2476-part-0/)
1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
Loading
Loading
@@ -176,7 +176,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
 
1. [Scalability and High Availability - Video](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
1. [High Availability - Video](https://www.youtube.com/watch?v=36KS808u6bE&index=15&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
1. [High Availability Documentation](https://about.gitlab.com/high-availability/)
1. [High Availability Documentation](https://about.gitlab.com/solutions/high-availability/)
 
### 3.8 Cycle Analytics
 
Loading
Loading
@@ -205,7 +205,7 @@ NOTE: **Note:**
Some content can only be accessed by GitLab team members.
 
1. [Support Path](support/README.md)
1. [Sales Path](https://about.gitlab.com/handbook/sales-onboarding/)
1. [Sales Path](https://about.gitlab.com/handbook/sales/onboarding/)
1. [User Training](training/user_training.md)
1. [GitLab Flow Training](training/gitlab_flow.md)
1. [Training Topics](training/index.md)
Loading
Loading
Loading
Loading
@@ -15,10 +15,10 @@ See the [book list](booklist.md) for additional recommendations.
1. **Remote: Office not required**
 
David Heinemeier Hansson and Jason Fried, 2013
([amazon](http://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673))
([amazon](https://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673))
 
1. **The Year Without Pants**
 
Scott Berkun, 2013 ([ScottBerkun.com](http://scottberkun.com/yearwithoutpants/))
Scott Berkun, 2013 ([ScottBerkun.com](https://scottberkun.com/yearwithoutpants/))
 
Any other books you'd like to suggest? Edit this page and add them to the queue.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment