Commit b5dbe0ba authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-08-09' into 'master'

CE upstream - Wednesday

Closes gitaly#402, omnibus-gitlab#2658, gitaly#443, gitaly#437, gitlab-com/infrastructure#1946, and gitaly#416

See merge request !2645
parents ef1470cb 6eb3fb4e
......@@ -30,6 +30,7 @@
"filenames/match-regex": [2, "^[a-z0-9_]+$"],
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error"
"promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}]
}
}
......@@ -41,6 +41,9 @@ linters:
ImplicitDiv:
enabled: true
InlineJavaScript:
enabled: true
InlineStyles:
enabled: false
......
......@@ -367,7 +367,7 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '0.2'
gem 'stackprof', '~> 0.2.10'
gem 'stackprof', '~> 0.2.10', require: false
end
group :test do
......@@ -418,7 +418,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly', '~> 0.24.0'
gem 'gitaly', '~> 0.26.0'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -294,7 +294,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.24.0)
gitaly (0.26.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -773,7 +773,6 @@ GEM
rubocop-gitlab-security (0.0.6)
rubocop (>= 0.47.1)
rubocop-rspec (1.15.1)
rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
......@@ -1019,7 +1018,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.24.0)
gitaly (~> 0.26.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -119,6 +119,12 @@ only be left until after the freeze if:
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
If a merge request is not ready, but the developers and Product Manager
responsible for the feature think it is essential that it is in the release,
they can [ask for an exception](#asking-for-an-exception) in advance. This is
preferable to merging something that we are not confident in, but should still
be a rare case: most features can be allowed to slip a release.
All Community Edition merge requests from GitLab team members merged on the
freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
......@@ -128,11 +134,26 @@ information, see
### After the 7th
Once the stable branch is frozen, only fixes for [regressions](#regressions)
and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
These fixes will be shipped in the next RC for that release if it is before the 22nd.
If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release.
Once the stable branch is frozen, the only MRs that can be cherry-picked into
the stable branch are:
* Fixes for [regressions](#regressions)
* Fixes for security issues
* New or updated translations (as long as they do not touch application code)
Any merge requests cherry-picked into the stable branch for a previous release
will also be picked into the latest stable branch. These fixes will be shipped
in the next RC for that release if it is before the 22nd. If the fixes are are
completed on or after the 22nd, they will be shipped in a patch for that
release.
During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label
~"Pick into Stable" set, so that release managers can find and pick them.
Merge requests without a milestone and this label will
not be merged into any stable branches.
### Asking for an exception
If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
......@@ -152,11 +173,7 @@ When in doubt, we err on the side of _not_ cherry-picking.
For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label
~"Pick into Stable" set, so that release managers can find and pick them.
Merge requests without a milestone and this label will
not be merged into any stable branches.
All MRs which have had exceptions granted must be merged by the 15th.
### Regressions
......
......@@ -14,6 +14,8 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
.replace(':id', groupId);
......@@ -95,6 +97,21 @@ const Api = {
.done(projects => callback(projects));
},
commitMultiple(id, data, callback) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', id);
return $.ajax({
url,
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data),
dataType: 'json',
})
.done(commitData => callback(commitData))
.fail(message => callback(message.responseJSON));
},
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
......
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
function toggleLoading($el, $icon, loading) {
if (loading) {
$el.disable();
$icon.removeClass(HIDDEN_CLASS);
} else {
$el.enable();
$icon.addClass(HIDDEN_CLASS);
}
}
export default class BlobFileDropzone {
constructor(form, method) {
const formDropzone = form.find('.dropzone');
const submitButton = form.find('#submit-all');
const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
const dropzoneMessage = form.find('.dz-message');
Dropzone.autoDiscover = false;
const dropzone = formDropzone.dropzone({
......@@ -26,12 +41,20 @@ export default class BlobFileDropzone {
},
init: function () {
this.on('addedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide();
});
this.on('removedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.removeClass(HIDDEN_CLASS);
});
this.on('success', function (header, response) {
window.location.href = response.filePath;
$('#modal-upload-blob').modal('hide');
window.gl.utils.visitUrl(response.filePath);
});
this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
......@@ -48,14 +71,15 @@ export default class BlobFileDropzone {
},
});
const submitButton = form.find('#submit-all')[0];
submitButton.addEventListener('click', function (e) {
submitButton.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
alert('Please select a file');
return false;
}
toggleLoading(submitButton, submitButtonLoadingIcon, true);
dropzone[0].dropzone.processQueue();
return false;
});
......
......@@ -97,9 +97,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!this.list) return true;
return !this.list.label || label.id !== this.list.label.id;
if (!this.list || !label) return true;
return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
......
......@@ -3,13 +3,13 @@ import $ from 'jquery';
// bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
import 'bootstrap-sass/assets/javascripts/bootstrap/button';
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
import 'bootstrap-sass/assets/javascripts/bootstrap/button';
// custom jQuery functions
$.fn.extend({
......
......@@ -81,6 +81,7 @@ import GpgBadges from './gpg_badges';
import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import UserFeatureHelper from './helpers/user_feature_helper';
// EE-only
import ApproversSelect from './approvers_select';
......@@ -104,6 +105,7 @@ import initGroupAnalytics from './init_group_analytics';
if (!page) {
return false;
}
path = page.split(':');
shortcut_handler = null;
......@@ -360,7 +362,6 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'projects:commits:show':
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit);
new gl.Activities();
shortcut_handler = new ShortcutsNavigation();
GpgBadges.fetch();
break;
......@@ -370,12 +371,10 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
if ($('#tree-slider').length) {
new TreeView();
}
if ($('.blob-viewer').length) {
new BlobViewer();
}
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
if ($('.project-show-activity').length) new gl.Activities();
initGeoInfoModal();
break;
case 'projects:edit':
......@@ -443,6 +442,9 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
if (UserFeatureHelper.isNewRepo()) break;
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
......@@ -469,6 +471,7 @@ import initGroupAnalytics from './init_group_analytics';
shortcut_handler = true;
break;
case 'projects:blob:show':
if (UserFeatureHelper.isNewRepo()) break;
new BlobViewer();
initBlob();
break;
......@@ -654,7 +657,6 @@ import initGroupAnalytics from './init_group_analytics';
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'), true);
new Sidebar();
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
......
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
/* global dateFormat */
/* global Pikaday */
import Pikaday from 'pikaday';
import DateFix from './lib/utils/datefix';
class DueDateSelect {
......
/* global bp */
import Cookies from 'js-cookie';
import './breakpoints';
export const canShowSubItems = () => bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const canShowActiveSubItems = (el) => {
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
if (el.classList.contains('active') && !isHiddenByMedia) {
return Cookies.get('sidebar_collapsed') === 'true';
}
return true;
};
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight;
......@@ -14,9 +24,10 @@ export const calculateTop = (boundingRect, outerHeight) => {
export const showSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!subItems || !canShowSubItems()) return;
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return;
subItems.style.display = 'block';
el.classList.add('is-showing-fly-out');
el.classList.add('is-over');
const boundingRect = el.getBoundingClientRect();
......@@ -34,15 +45,16 @@ export const showSubLevelItems = (el) => {
export const hideSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!subItems || !canShowSubItems()) return;
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return;
el.classList.remove('is-showing-fly-out');
el.classList.remove('is-over');
subItems.style.display = 'none';
subItems.classList.remove('is-above');
};
export default () => {
const items = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active)')]
const items = [...document.querySelectorAll('.sidebar-top-level-items > li')]
.filter(el => el.querySelector('.sidebar-sub-level-items'));
items.forEach((el) => {
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote;
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
GitLabDropdownInput = (function() {
function GitLabDropdownInput(input, options) {
var $inputContainer, $clearButton;
var _this = this;
this.input = input;
this.options = options;
this.fieldName = this.options.fieldName || 'field-name';
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('input').focus();
};
})(this));
this.input
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', function(e) {
var val = e.currentTarget.value || _this.options.inputFieldName;
val = val.split(' ').join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
_this.cb(_this.options.fieldName, val, {}, true);
_this.input.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
}
GitLabDropdownInput.prototype.onInput = function(cb) {
this.cb = cb;
};
return GitLabDropdownInput;
})();
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
......@@ -191,7 +235,7 @@ GitLabDropdownRemote = (function() {
})();
GitLabDropdown = (function() {
var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading";
......@@ -209,7 +253,9 @@ GitLabDropdown = (function() {
CURSOR_SELECT_SCROLL_PADDING = 5;
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
function GitLabDropdown(el1, options) {
var searchFields, selector, self;
......@@ -224,6 +270,7 @@ GitLabDropdown = (function() {
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
......@@ -262,6 +309,10 @@ GitLabDropdown = (function() {
});
}
}
if (this.noFilterInput.length) {
this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
this.plainInput.onInput(this.addInput.bind(this));
}
// Init filterable
if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, {
......@@ -768,9 +819,13 @@ GitLabDropdown = (function() {
}
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) {
var $input;
// Create hidden input for form
if (single) {
$('input[name="' + fieldName + '"]').remove();
}
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
......@@ -786,7 +841,7 @@ GitLabDropdown = (function() {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
return this.dropdown.before($input);
this.dropdown.before($input).trigger('change');
};
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
......
import Chart from 'vendor/Chart';
import _ from 'underscore';
document.addEventListener('DOMContentLoaded', () => {
const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML);
......@@ -27,28 +28,25 @@ document.addEventListener('DOMContentLoaded', () => {
return generateChart();
};
const chartData = (keys, values) => {
const data = {
labels: keys,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: values,
}],
};
return data;
};
const hourData = chartData(projectChartData.hour.keys, projectChartData.hour.values);
const chartData = data => ({
labels: Object.keys(data),
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: _.values(data),
}],
});
const hourData = chartData(projectChartData.hour);
responsiveChart($('#hour-chart'), hourData);
const dayData = chartData(projectChartData.weekDays.keys, projectChartData.weekDays.values);
const dayData = chartData(projectChartData.weekDays);
responsiveChart($('#weekday-chart'), dayData);
const monthData = chartData(projectChartData.month.keys, projectChartData.month.values);
const monthData = chartData(projectChartData.month);
responsiveChart($('#month-chart'), monthData);
const data = projectChartData.languages;
......
import Cookies from 'js-cookie';
function isNewRepo() {
return Cookies.get('new_repo') === 'true';
}
const UserFeatureHelper = {
isNewRepo,
};
export default UserFeatureHelper;
......@@ -3,8 +3,8 @@
/* global Autosave */
/* global GroupsSelect */
/* global dateFormat */
/* global Pikaday */
import Pikaday from 'pikaday';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
......
/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
export const HIDDEN_CLASS = 'hidden';
......@@ -7,7 +7,6 @@
import jQuery from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import Pikaday from 'pikaday';
import Dropzone from 'dropzone';
import Sortable from 'vendor/Sortable';
......@@ -20,7 +19,6 @@ import 'vendor/fuzzaldrin-plus';
window.jQuery = jQuery;
window.$ = jQuery;
window._ = _;
window.Pikaday = Pikaday;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
......
/* global Pikaday */
/* global dateFormat */
import Pikaday from 'pikaday';
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
......
......@@ -45,8 +45,10 @@ export default class NewNavSidebar {
toggleCollapsedSidebar(collapsed) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-new-sidebar', !collapsed);
this.$page.toggleClass('page-with-icon-sidebar', collapsed);
if (this.$sidebar.length) {
this.$page.toggleClass('page-with-new-sidebar', !collapsed);
this.$page.toggleClass('page-with-icon-sidebar', collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
}
......
......@@ -9,8 +9,8 @@
</template>
<script>
import pdfjsLib from 'pdfjs-dist';
import workerSrc from 'vendor/pdf.worker';
import pdfjsLib from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
import page from './page/index.vue';
......
......@@ -102,6 +102,7 @@ import Cookies from 'js-cookie';
filterable: true,
filterRemote: true,
filterByText: true,
inputFieldName: $dropdown.data('input-field-name'),
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var li = refListItem.cloneNode(false);
......@@ -135,9 +136,14 @@ import Cookies from 'js-cookie';
e.preventDefault();
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
var $visit = $dropdown.data('visit');
var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
if (shouldVisit) {
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
}
});
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
(function () {
this.ProjectSelect = (function () {
......@@ -50,20 +51,19 @@ import Api from './api';
});
$('.ajax-project-select').each(function (i, select) {
var placeholder;
var idAttribute;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('allprojects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
idAttribute = $(select).data('idattribute') || 'web_url';
placeholder = "Search for project";
if (this.includeGroups) {
placeholder += " or group";
}
return $(select).select2({
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function (_this) {
......@@ -101,22 +101,19 @@ import Api from './api';
}
};
})(this),
id: function (project) {
return project[idAttribute];
id: function(project) {
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
});
$('.new-project-item-select-button').on('click', function() {
$('.project-item-select', this.parentNode).select2('open');
});
$('.project-item-select').on('click', function() {
window.location = `${$(this).val()}/${this.dataset.relativePath}`;
return new ProjectSelectComboButton(select);
});
}
......
import AccessorUtilities from './lib/utils/accessor';
export default class ProjectSelectComboButton {
constructor(select) {
this.projectSelectInput = $(select);
this.newItemBtn = $('.new-project-item-link');
this.newItemBtnBaseText = this.newItemBtn.data('label');
this.itemType = this.deriveItemTypeFromLabel();
this.groupId = this.projectSelectInput.data('groupId');
this.bindEvents();
this.initLocalStorage();
}
bindEvents() {
this.projectSelectInput.siblings('.new-project-item-select-button')
.on('click', this.openDropdown);
this.projectSelectInput.on('change', () => this.selectProject());
}
initLocalStorage() {
const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
if (localStorageIsSafe) {
const itemTypeKebabed = this.newItemBtnBaseText.toLowerCase().split(' ').join('-');
this.localStorageKey = ['group', this.groupId, itemTypeKebabed, 'recent-project'].join('-');
this.setBtnTextFromLocalStorage();
}
}
openDropdown() {
$(this).siblings('.project-item-select').select2('open');
}
selectProject() {
const selectedProjectData = JSON.parse(this.projectSelectInput.val());
const projectUrl = `${selectedProjectData.url}/${this.projectSelectInput.data('relativePath')}`;
const projectName = selectedProjectData.name;
const projectMeta = {
url: projectUrl,
name: projectName,
};
this.setNewItemBtnAttributes(projectMeta);
this.setProjectInLocalStorage(projectMeta);
}
setBtnTextFromLocalStorage() {
const cachedProjectData = this.getProjectFromLocalStorage();
this.setNewItemBtnAttributes(cachedProjectData);
}
setNewItemBtnAttributes(project) {
if (project) {
this.newItemBtn.attr('href', project.url);
this.newItemBtn.text(`${this.newItemBtnBaseText} in ${project.name}`);
this.newItemBtn.enable();
} else {
this.newItemBtn.text(`Select project to create ${this.itemType}`);
this.newItemBtn.disable();
}
}
deriveItemTypeFromLabel() {
// label is either 'New issue' or 'New merge request'
return this.newItemBtnBaseText.split(' ').slice(1).join(' ');
}
getProjectFromLocalStorage() {
const projectString = localStorage.getItem(this.localStorageKey);
return JSON.parse(projectString);
}
setProjectInLocalStorage(projectMeta) {
const projectString = JSON.stringify(projectMeta);
localStorage.setItem(this.localStorageKey, projectString);
}
}
import '../lib/utils/url_utility';
const bindEvents = () => {
const path = gl.utils.getParameterValues('path')[0];
// get the path url and append it in the inputS
$('.js-path-name').val(path);
};
document.addEventListener('DOMContentLoaded', bindEvents);
export default {
bindEvents,
};
let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
if ($projectImportUrl.attr('disabled') || hasUserDefinedProjectPath) {
if (hasUserDefinedProjectPath) {
return;
}
......@@ -27,8 +27,6 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const importBtnTooltip = 'Please enter a valid project name.';
const $importBtnWrapper = $('.import_gitlab_project');
const $projectImportUrl = $('#project_import_url');
const $projectPath = $('#project_path');
......@@ -50,31 +48,15 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
});
$('.btn_import_gitlab_project').attr('disabled', !$projectPath.val().trim().length);
$importBtnWrapper.attr('title', importBtnTooltip);
$newProjectForm.on('submit', () => {
$projectPath.val($projectPath.val().trim());
});
$projectPath.on('keyup', () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
if (hasUserDefinedProjectPath) {
$('.btn_import_gitlab_project').attr('disabled', false);
$importBtnWrapper.attr('title', '');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled', true);
$importBtnWrapper.addClass('has-tooltip');
}
});
$projectImportUrl.disable();
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
$('.import_git').on('click', () => {
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
};
document.addEventListener('DOMContentLoaded', bindEvents);
......
<script>
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
export default {
data: () => Store,
mixins: [RepoMixin],
components: {
'repo-sidebar': RepoSidebar,
'repo-tabs': RepoTabs,
'repo-file-buttons': RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
'repo-commit-section': RepoCommitSection,
'popup-dialog': PopupDialog,
'repo-preview': RepoPreview,
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
methods: {
dialogToggled(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.dialog.open = false;
this.dialog.status = status;
},
toggleBlobView: Store.toggleBlobView,
},
};
</script>
<template>
<div class="repository-view tree-content-holder">
<repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
<repo-tabs/>
<component :is="currentBlobView" class="blob-viewer-container"></component>
<repo-file-buttons/>
</div>
<repo-commit-section/>
<popup-dialog
:primary-button-label="__('Discard changes')"
:open="dialog.open"
kind="warning"
:title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')"
@toggle="dialogToggled"
@submit="dialogSubmitted"
/>
</div>
</template>
<script>
/* global Flash */
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoCommitSection = {
data: () => Store,
mixins: [RepoMixin],
computed: {
branchPaths() {
const branch = Helper.getBranch();
return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
},
cantCommitYet() {
return !this.commitMessage || this.submitCommitsLoading;
},
filePluralize() {
return this.changedFiles.length > 1 ? 'files' : 'file';
},
},
methods: {
makeCommit() {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const branch = Helper.getBranch();
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: 'update',
file_path: Helper.getFilePathFromFullPath(f.url, branch),
content: f.newContent,
}));
const payload = {
branch: Store.targetBranch,
commit_message: commitMessage,
actions,
};
Store.submitCommitsLoading = true;
Service.commitFiles(payload, this.resetCommitState);
},
resetCommitState() {
this.submitCommitsLoading = false;
this.changedFiles = [];
this.openedFiles = [];
this.commitMessage = '';
this.editMode = false;
$('html, body').animate({ scrollTop: 0 }, 'fast');
},
},
};
export default RepoCommitSection;
</script>
<template>
<div id="commit-area" v-if="isCommitable && changedFiles.length" >
<form class="form-horizontal">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
<div class="col-md-4">
<ul class="list-unstyled changed-files">
<li v-for="file in branchPaths" :key="file.id">
<span class="help-block">{{file}}</span>
</li>
</ul>
</div>
</div>
<!-- Textarea
-->
<div class="form-group">
<label class="col-md-4 control-label" for="commit-message">Commit message</label>
<div class="col-md-4">
<textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
</div>
</div>
<!-- Button Drop Down
-->
<div class="form-group target-branch">
<label class="col-md-4 control-label" for="target-branch">Target branch</label>
<div class="col-md-4">
<span class="help-block">{{targetBranch}}</span>
</div>
</div>
<div class="col-md-offset-4 col-md-4">
<button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
<i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
<span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
</button>
</div>
</fieldset>
</form>
</div>
</template>
<script>
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
export default {
data: () => Store,
mixins: [RepoMixin],
computed: {
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
buttonIcon() {
return this.editMode ? [] : ['fa', 'fa-pencil'];
},
},
methods: {
editClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
},
watch: {
editMode() {
if (this.editMode) {
$('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show();
$('.project-refs-target-form').show();
} else {
$('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide();
$('.project-refs-target-form').hide();
}
},
},
};
</script>
<template>
<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
<i :class="buttonIcon"></i>
<span>{{buttonLabel}}</span>
</button>
</template>
<script>
/* global monaco */
import Store from '../stores/repo_store';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
const RepoEditor = {
data: () => Store,
destroyed() {
// this.monacoInstance.getModels().forEach((m) => {
// m.dispose();
// });
this.monacoInstance.destroy();
},
mounted() {
Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
const monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: false,
});
Store.monacoInstance = monacoInstance;
this.addMonacoEvents();
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
this.showHide();
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}).catch(Helper.loadingError);
},
methods: {
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
},
addMonacoEvents() {
this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
Store.setActiveFileContents(this.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
const lineNumber = e.target.position.lineNumber;
if (e.target.element.className === 'line-numbers') {
location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber;
}
},
},
watch: {
activeLine() {
this.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
},
activeFileLabel() {
this.showHide();
},
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
}
f.changed = false;
delete f.newContent;
return f;
});
this.editMode = false;
}
},
deep: true,
},
isTree() {
this.showHide();
},
openedFiles() {
this.showHide();
},
binary() {
this.showHide();
},
blobRaw() {
this.showHide();
if (this.isTree) return;
this.monacoInstance.setModel(null);
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
},
},
};
export default RepoEditor;
</script>
<template>
<div id="ide"></div>
</template>
<script>
import TimeAgoMixin from '../../vue_shared/mixins/timeago';
const RepoFile = {
mixins: [TimeAgoMixin],
props: {
file: {
type: Object,
required: true,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
loading: {
type: Object,
required: false,
default() { return { tree: false }; },
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
activeFile: {
type: Object,
required: true,
},
},
computed: {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
},
},
};
export default RepoFile;
</script>
<template>
<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
<td @click.prevent="linkClicked(file)">
<i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
<i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
<a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs">
<div class="commit-message">
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
</div>
</td>
<td v-if="!isMini" class="hidden-xs">
<span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
</td>
</tr>
</template>
<script>
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data: () => Store,
mixins: [RepoMixin],
computed: {
rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw';
},
canPreview() {
return Helper.isKindaBinary();
},
},
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
};
export default RepoFileButtons;
</script>
<template>
<div id="repo-file-buttons" v-if="isMini">
<a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
<div class="btn-group" role="group" aria-label="File actions">
<a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
<a :href="activeFile.commits_path" class="btn btn-default history">History</a>
<a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
</div>
<a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
</div>
</template>
<script>
const RepoFileOptions = {
props: {
isMini: {
type: Boolean,
required: false,
default: false,
},
projectName: {
type: String,
required: true,
},
},
};
export default RepoFileOptions;
</script>
<template>
<tr v-if="isMini" class="repo-file-options">
<td>
<span class="title">{{projectName}}</span>
</td>
</tr>
</template>
<script>
const RepoLoadingFile = {
props: {
loading: {
type: Object,
required: false,
default: {},
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
lineOfCode(n) {
return `line-of-code-${n}`;
},
},
};
export default RepoLoadingFile;
</script>
<template>
<tr v-if="loading.tree && !hasFiles" class="loading-file">
<td>
<div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
</div>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs">
<div class="animation-container">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
</div>
</td>
<td v-if="!isMini" class="hidden-xs">
<div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
</div>
</td>
</tr>
</template>
<script>
const RepoPreviousDirectory = {
props: {
prevUrl: {
type: String,
required: true,
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
},
},
};
export default RepoPreviousDirectory;
</script>
<template>
<tr class="prev-directory">
<td colspan="3">
<a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
</td>
</tr>
</template>
<script>
import Store from '../stores/repo_store';
export default {
data: () => Store,
mounted() {
$(this.$el).find('.file-content').syntaxHighlight();
},
computed: {
html() {
return this.activeFile.html;
},
},
watch: {
html() {
this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight();
});
},
},
};
</script>
<template>
<div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
<div v-if="activeFile.render_error" class="vertical-center render-error">
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
</div>
</div>
</template>
<script>
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFileOptions from './repo_file_options.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
const RepoSidebar = {
mixins: [RepoMixin],
components: {
'repo-file-options': RepoFileOptions,
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
this.addPopEventListener();
},
data: () => Store,
methods: {
addPopEventListener() {
window.addEventListener('popstate', () => {
if (location.href.indexOf('#') > -1) return;
this.linkClicked({
url: location.href,
});
});
},
linkClicked(clickedFile) {
let url = '';
let file = clickedFile;
if (typeof file === 'object') {
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
url = file.url;
Service.url = url;
// I need to refactor this to do the `then` here.
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false;
Helper.scrollTabsRight();
});
}
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
}
},
},
};
export default RepoSidebar;
</script>
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
<table class="table">
<thead v-if="!isMini">
<tr>
<th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th>
<th class="hidden-xs last-update">Last Update</th>
</tr>
</thead>
<tbody>
<repo-file-options
:is-mini="isMini"
:project-name="projectName"/>
<repo-previous-directory
v-if="isRoot"
:prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/>
<repo-loading-file
v-for="n in 5"
:key="n"
:loading="loading"
:has-files="!!files.length"
:is-mini="isMini"/>
<repo-file
v-for="file in files"
:key="file.id"
:file="file"
:is-mini="isMini"
@linkclicked="linkClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"/>
</tbody>
</table>
</div>
</template>
<script>
import Store from '../stores/repo_store';
const RepoTab = {
props: {
tab: {
type: Object,
required: true,
},
},
computed: {
changedClass() {
const tabChangedObj = {
'fa-times': !this.tab.changed,
'fa-circle': this.tab.changed,
};
return tabChangedObj;
},
},
methods: {
tabClicked: Store.setActiveFiles,
xClicked(file) {
if (file.changed) return;
this.$emit('xclicked', file);
},
},
};
export default RepoTab;
</script>
<template>
<li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
<i class="fa" :class="changedClass"></i>
</a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
</li>
</template>
<script>
import Vue from 'vue';
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
const RepoTabs = {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
data: () => Store,
methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) {
Store.removeFromOpenedFiles(file);
},
},
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
};
export default RepoTabs;
</script>
<template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" />
</ul>
</template>
/* global monaco */
import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
import monacoLoader from '../monaco_loader';
function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => {
Store.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, reject);
});
}
const MonacoLoaderHelper = {
repoEditorLoader,
};
export default MonacoLoaderHelper;
/* global Flash */
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import '../../flash';
const RepoHelper = {
getDefaultActiveFile() {
return {
active: true,
binary: false,
extension: '',
html: '',
mime_type: '',
name: '',
plain: '',
size: 0,
url: '',
raw: false,
newContent: '',
changed: false,
loading: false,
};
},
key: '',
isTree(data) {
return Object.hasOwnProperty.call(data, 'blobs');
},
Time: window.performance
&& window.performance.now
? window.performance
: Date,
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
},
getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop();
const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext';
},
getFilePathFromFullPath(fullPath, branch) {
return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
},
findLanguage(ext, langs) {
return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
},
setDirectoryOpen(tree) {
const file = tree;
if (!file) return undefined;
file.opened = true;
file.icon = 'fa-folder-open';
RepoHelper.toURL(file.url, file.name);
return file;
},
isKindaBinary() {
const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1;
},
setBinaryDataAsBase64(file) {
Service.getBase64Content(file.raw_path)
.then((response) => {
Store.blobRaw = response;
file.base64 = response; // eslint-disable-line no-param-reassign
})
.catch(RepoHelper.loadingError);
},
toggleFakeTab(loading, file) {
if (loading) return Store.addPlaceholderFile();
return Store.removeFromOpenedFiles(file);
},
setLoading(loading, file) {
if (Service.url.indexOf('blob') > -1) {
Store.loading.blob = loading;
return RepoHelper.toggleFakeTab(loading, file);
}
if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
return undefined;
},
getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted;
const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
if (!indexOfFile) return newListSorted;
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
},
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1;
const file = newFile;
file.level = inDirectory.level + 1;
oldList.splice(fileIndex, 0, file);
});
return oldList;
},
compareFilesCaseInsensitive(a, b) {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
if (a.level > 0) return 0;
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
isRoot(url) {
// the url we are requesting -> split by the project URL. Grab the right side.
const isRoot = !!url.split(Store.projectUrl)[1]
// remove the first "/"
.slice(1)
// split this by "/"
.split('/')
// remove the first two items of the array... usually /tree/master.
.slice(2)
// we want to know the length of the array.
// If greater than 0 not root.
.length;
return isRoot;
},
getContent(treeOrFile, cb) {
let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
// RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
Store.binary = data.binary;
if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
} else if (!Store.isPreviewView()) {
if (!data.render_error) {
Service.getRaw(data.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data;
RepoHelper.setFile(data, file);
}).catch(RepoHelper.loadingError);
}
}
if (Store.isPreviewView()) {
RepoHelper.setFile(data, file);
}
// if the file tree is empty
if (Store.files.length === 0) {
const parentURL = Service.blobURLtoParentTree(Service.url);
Service.url = parentURL;
RepoHelper.getContent();
}
} else {
// it's a tree
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
file = RepoHelper.setDirectoryOpen(file);
const newDirectory = RepoHelper.dataToListOfFiles(data);
Store.addFilesToDirectory(file, Store.files, newDirectory);
Store.prevURL = Service.blobURLtoParentTree(Service.url);
}
}).catch(RepoHelper.loadingError);
},
setFile(data, file) {
const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url;
if (newFile.render_error === 'too_large') {
newFile.tooLarge = true;
}
newFile.newContent = '';
Store.addToOpenedFiles(newFile);
Store.setActiveFiles(newFile);
},
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
simpleBlob.loading = false;
return simpleBlob;
},
serializeTree(tree) {
return RepoHelper.serializeRepoEntity('tree', tree);
},
serializeSubmodule(submodule) {
return RepoHelper.serializeRepoEntity('submodule', submodule);
},
serializeRepoEntity(type, entity) {
const { url, name, icon, last_commit } = entity;
const returnObj = {
type,
name,
url,
icon: RepoHelper.toFA(icon),
level: 0,
loading: false,
};
if (entity.last_commit) {
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
} else {
returnObj.lastCommitUrl = '';
}
return returnObj;
},
scrollTabsRight() {
// wait for the transition. 0.1 seconds.
setTimeout(() => {
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = 12000;
}, 200);
},
dataToListOfFiles(data) {
const a = [];
// push in blobs
data.blobs.forEach((blob) => {
a.push(RepoHelper.serializeBlob(blob));
});
data.trees.forEach((tree) => {
a.push(RepoHelper.serializeTree(tree));
});
data.submodules.forEach((submodule) => {
a.push(RepoHelper.serializeSubmodule(submodule));
});
return a;
},
genKey() {
return RepoHelper.Time.now().toFixed(3);
},
getStateKey() {
return RepoHelper.key;
},
setStateKey(key) {
RepoHelper.key = key;
},
toURL(url, title) {
const history = window.history;
RepoHelper.key = RepoHelper.genKey();
history.pushState({ key: RepoHelper.key }, '', url);
if (title) {
document.title = `${title} · GitLab`;
}
},
findOpenedFileFromActive() {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
},
loadingError() {
Flash('Unable to load the file at this time.');
},
};
export default RepoHelper;
import $ from 'jquery';
import Vue from 'vue';
import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.project-refs-target-form').hide();
$('.fa-long-arrow-right').hide();
}
function addEventsForNonVueEls() {
$(document).on('change', '.dropdown', () => {
Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
});
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
if (!hasChanged) return undefined;
const event = e || window.event;
if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
// For Safari
return 'Are you sure you want to lose unsaved changes?';
};
}
function setInitialStore(data) {
Store.service = Service;
Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl;
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
}
function initRepo(el) {
return new Vue({
el,
components: {
repo: Repo,
},
});
}
function initRepoEditButton(el) {
return new Vue({
el,
components: {
repoEditButton: RepoEditButton,
},
});
}
function initRepoBundle() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
Vue.use(Translate);
initRepo(repo);
initRepoEditButton(editButton);
}
$(initRepoBundle);
export default initRepoBundle;
import Store from '../stores/repo_store';
const RepoMixin = {
computed: {
isMini() {
return !!Store.openedFiles.length;
},
changedFiles() {
const changedFileList = this.openedFiles
.filter(file => file.changed);
return changedFileList;
},
},
};
export default RepoMixin;
/* eslint-disable no-underscore-dangle, camelcase */
/* global __webpack_public_path__ */
import monacoContext from 'monaco-editor/dev/vs/loader';
monacoContext.require.config({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`,
},
});
window.__monaco_context__ = monacoContext;
export default monacoContext.require;
/* global Flash */
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
const RepoService = {
url: '',
options: {
params: {
format: 'json',
},
},
richExtensionRegExp: /md/,
checkCurrentBranchIsCommitable() {
const url = Store.service.refsUrl;
return axios.get(url, { params: {
ref: Store.currentBranch,
search: Store.currentBranch,
} });
},
getRaw(url) {
return axios.get(url, {
transformResponse: [res => res],
});
},
buildParams(url = this.url) {
// shallow clone object without reference
const params = Object.assign({}, this.options.params);
if (this.urlIsRichBlob(url)) params.viewer = 'rich';
return params;
},
urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop();
return this.richExtensionRegExp.test(extension);
},
getContent(url = this.url) {
const params = this.buildParams(url);
return axios.get(url, {
params,
});
},
getBase64Content(url = this.url) {
const request = axios.get(url, {
responseType: 'arraybuffer',
});
return request.then(response => this.bufferToBase64(response.data));
},
bufferToBase64(data) {
return new Buffer(data, 'binary').toString('base64');
},
blobURLtoParentTree(url) {
const urlArray = url.split('/');
urlArray.pop();
const blobIndex = urlArray.lastIndexOf('blob');
if (blobIndex > -1) urlArray[blobIndex] = 'tree';
return urlArray.join('/');
},
commitFiles(payload, cb) {
Api.commitMultiple(Store.projectId, payload, (data) => {
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
cb();
});
},
};
export default RepoService;
/* global Flash */
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
ideEl: {},
monaco: {},
monacoLoading: false,
monacoInstance: {},
service: '',
editor: '',
sidebar: '',
editMode: false,
isTree: false,
isRoot: false,
prevURL: '',
projectId: '',
projectName: '',
projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview',
openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false,
binaryLoaded: false,
dialog: {
open: false,
title: '',
status: false,
},
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
activeLine: 0,
activeFileLabel: 'Raw',
files: [],
isCommitable: false,
binary: false,
currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: {
png: false,
md: false,
svg: false,
unknown: false,
},
loading: {
tree: false,
blob: false,
},
readOnly: true,
resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => {
RepoStore.binaryTypes[key] = false;
});
},
// mutations
checkIsCommitable() {
RepoStore.service.checkCurrentBranchIsCommitable()
.then((data) => {
// you shouldn't be able to make commits on commits or tags.
const { Branches, Commits, Tags } = data.data;
if (Branches && Branches.length) RepoStore.isCommitable = true;
if (Commits && Commits.length) RepoStore.isCommitable = false;
if (Tags && Tags.length) RepoStore.isCommitable = false;
}).catch(() => Flash('Failed to check if branch can be committed to.'));
},
addFilesToDirectory(inDirectory, currentList, newList) {
RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
},
toggleRawPreview() {
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
},
setActiveFiles(file) {
if (RepoStore.isActiveFile(file)) return;
RepoStore.openedFiles = RepoStore.openedFiles
.map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
RepoStore.setActiveToRaw();
if (file.binary) {
RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain;
} else {
Service.getRaw(file.raw_path)
.then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError);
}
if (!file.loading) Helper.toURL(file.url, file.name);
RepoStore.binary = file.binary;
},
setFileActivity(file, openedFile, i) {
const activeFile = openedFile;
activeFile.active = file.url === activeFile.url;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
return activeFile;
},
setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
RepoStore.activeFileIndex = i;
},
setActiveToRaw() {
RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now.
RepoStore.activeFileLabel = 'Display source';
},
removeChildFilesOfTree(tree) {
let foundTree = false;
const treeToClose = tree;
let wereDone = false;
RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
wereDone = true;
return true;
}
if (wereDone) return true;
if (isItTheTreeWeWant) foundTree = true;
if (foundTree) return file.level <= treeToClose.level;
return true;
});
treeToClose.opened = false;
treeToClose.icon = 'fa-folder';
return treeToClose;
},
removeFromOpenedFiles(file) {
if (file.type === 'tree') return;
let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
if (openedFile.url === file.url) foundIndex = i;
return openedFile.url !== file.url;
});
// now activate the right tab based on what you closed.
if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {};
return;
}
if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
return;
}
if (foundIndex) {
if (foundIndex > 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
}
}
},
addPlaceholderFile() {
const randomURL = Helper.Time.now();
const newFakeFile = {
active: false,
binary: true,
type: 'blob',
loading: true,
mime_type: 'loading',
name: 'loading',
url: randomURL,
fake: true,
};
RepoStore.openedFiles.push(newFakeFile);
return newFakeFile;
},
addToOpenedFiles(file) {
const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles
.some(openedFile => openedFile.url === openFile.url);
if (openedFilesAlreadyExists) return;
openFile.changed = false;
RepoStore.openedFiles.push(openFile);
},
setActiveFileContents(contents) {
if (!RepoStore.editMode) return;
const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
RepoStore.activeFile.newContent = contents;
RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
currentFile.changed = RepoStore.activeFile.changed;
currentFile.newContent = contents;
},
toggleBlobView() {
RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
},
setViewToPreview() {
RepoStore.currentBlobView = 'repo-preview';
},
// getters
isActiveFile(file) {
return file && file.url === RepoStore.activeFile.url;
},
isPreviewView() {
return RepoStore.currentBlobView === 'repo-preview';
},
};
export default RepoStore;
import 'core-js/es6/map';
import 'core-js/es6/set';
import simulateDrag from './simulate_drag';
// Export to global space for rspec to use
......
import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'MRWidgetAuthor',
props: {
......@@ -5,6 +7,9 @@ export default {
showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false },
},
directives: {
tooltip,
},
template: `
<a
:href="author.webUrl || author.web_url"
......
import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility';
export default {
......@@ -5,6 +6,9 @@ export default {
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
......@@ -29,18 +33,51 @@ export default {
},
template: `
<div class="mr-source-target">
<div
v-if="mr.isOpen"
class="pull-right">
<div class="normal">
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
v-html="mr.sourceBranchLink"></span>
<button
v-tooltip
class="btn btn-transparent btn-clipboard"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
:class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
<div v-if="mr.isOpen">
<a
href="#modal_merge_info"
data-toggle="modal"
class="btn inline btn-grouped">
class="btn btn-small inline">
Check out branch
</a>
<span class="dropdown inline prepend-left-10">
<a
class="btn dropdown-toggle"
class="btn btn-xs dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
......@@ -69,38 +106,6 @@ export default {
</ul>
</span>
</div>
<div class="normal">
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
</div>
`,
};
......@@ -2,20 +2,18 @@ export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: { type: Object, required: true },
state: { type: String },
state: { type: String, required: false },
},
computed: {
hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe;
},
},
methods: {
closesText(state) {
if (state === 'merged') {
closesText() {
if (this.state === 'merged') {
return 'Closed';
}
if (state === 'closed') {
if (this.state === 'closed') {
return 'Did not close';
}
return 'Closes';
......@@ -26,7 +24,7 @@ export default {
v-if="hasLinks"
class="mr-info-list mr-links">
<p v-if="relatedLinks.closing">
{{closesText(state)}} <span v-html="relatedLinks.closing"></span>
{{closesText}} <span v-html="relatedLinks.closing"></span>
</p>
<p v-if="relatedLinks.mentioned">
Mentions <span v-html="relatedLinks.mentioned"></span>
......
......@@ -15,7 +15,7 @@ export default {
<span class="bold">
There are merge conflicts<span v-if="!mr.canMerge">.</span>
<span v-if="!mr.canMerge">
Resolve these conflicts or ask someone with write access to this repository to merge it locally.
Resolve these conflicts or ask someone with write access to this repository to merge it locally
</span>
</span>
<a
......
/* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
......@@ -10,15 +12,19 @@ export default {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
statusIcon,
},
data() {
return {
isMakingRequest: false,
};
},
directives: {
tooltip,
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
loadingIcon,
statusIcon,
},
computed: {
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
......@@ -68,7 +74,8 @@ export default {
:dateReadable="mr.mergedAt" />
<a
v-if="mr.canRevertInCurrentMR"
class="btn btn-close btn-xs has-tooltip"
v-tooltip
class="btn btn-close btn-xs"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
......@@ -77,7 +84,8 @@ export default {
</a>
<a
v-else-if="mr.revertInForkPath"
class="btn btn-close btn-xs has-tooltip"
v-tooltip
class="btn btn-close btn-xs"
data-method="post"
:href="mr.revertInForkPath"
title="Revert this merge request in a new merge request">
......@@ -85,7 +93,8 @@ export default {
</a>
<a
v-if="mr.canCherryPickInCurrentMR"
class="btn btn-default btn-xs has-tooltip"
v-tooltip
class="btn btn-default btn-xs"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
......@@ -94,7 +103,8 @@ export default {
</a>
<a
v-else-if="mr.cherryPickInForkPath"
class="btn btn-default btn-xs has-tooltip"
v-tooltip
class="btn btn-default btn-xs"
data-method="post"
:href="mr.cherryPickInForkPath"
title="Cherry-pick this merge request in a new merge request">
......@@ -113,14 +123,14 @@ export default {
<span>You can remove source branch now</span>
<button
@click="removeSourceBranch"
:class="{ disabled: isMakingRequest }"
:disabled="isMakingRequest"
type="button"
class="btn btn-xs btn-default js-remove-branch-button">
Remove Source Branch
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<status-icon status="loading" />
<loading-icon inline />
<span>The source branch is being removed</span>
</p>
</section>
......
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
export default {
......@@ -6,6 +7,9 @@ export default {
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
statusIcon,
......@@ -28,8 +32,10 @@ export default {
</span> branch does not exist.
Please restore it or use a different {{missingBranchName}} branch
<i
class="fa fa-question-circle has-tooltip"
:title="message" />
v-tooltip
class="fa fa-question-circle"
:title="message"
:aria-label="message"></i>
</span>
</div>
</div>
......
/* global Flash */
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
export default {
......@@ -8,11 +9,17 @@ export default {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
directives: {
tooltip,
},
data() {
return {
isMakingRequest: false,
};
},
components: {
statusIcon,
},
methods: {
removeWIP() {
this.isMakingRequest = true;
......@@ -29,9 +36,6 @@ export default {
});
},
},
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
......@@ -39,8 +43,11 @@ export default {
<span class="bold">
This is a Work in Progress
<i
class="fa fa-question-circle has-tooltip"
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged." />
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
......
......@@ -38,6 +38,7 @@ export default {
props: {
mrData: {
type: Object,
required: false,
},
},
data() {
......@@ -247,7 +248,9 @@ export default {
:state="mr.state"
:related-links="mr.relatedLinks" />
</div>
<div class="mr-widget-footer" v-if="shouldRenderMergeHelp">
<div
class="mr-widget-footer"
v-if="shouldRenderMergeHelp">
<mr-widget-merge-help />
</div>
</div>
......
<script>
const PopupDialog = {
name: 'popup-dialog',
props: {
open: Boolean,
title: String,
body: String,
kind: {
type: String,
default: 'primary',
},
closeButtonLabel: {
type: String,
default: 'Cancel',
},
primaryButtonLabel: {
type: String,
default: 'Save changes',
},
},
computed: {
typeOfClass() {
const className = `btn-${this.kind}`;
const returnObj = {};
returnObj[className] = true;
return returnObj;
},
},
methods: {
close() {
this.$emit('toggle', false);
},
yesClick() {
this.$emit('submit', true);
},
noClick() {
this.$emit('submit', false);
},
},
};
export default PopupDialog;
</script>
<template>
<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<p>{{this.body}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
<button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
</div>
</div>
</div>
</div>
</template>
......@@ -104,6 +104,8 @@
margin: 0;
align-self: center;
}
&.s40 { min-width: 40px; min-height: 40px; }
}
.avatar-counter {
......
......@@ -381,6 +381,10 @@ table {
background: $gl-success !important;
}
.dz-message {
margin: 0;
}
.space-right {
margin-right: 10px;
}
......
......@@ -26,7 +26,7 @@ header {
&.navbar-gitlab {
padding: 0 16px;
z-index: 400;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
background-color: $gray-light;
......
......@@ -116,3 +116,13 @@ body {
.with-performance-bar .page-with-sidebar {
margin-top: $header-height + $performance-bar-height;
}
[v-cloak] {
display: none;
}
.vertical-center {
min-height: 100vh;
display: flex;
align-items: center;
}
......@@ -251,7 +251,6 @@
// Applies on /dashboard/issues
.project-item-select-holder {
display: block;
margin: 0;
}
......@@ -310,6 +309,31 @@
}
}
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
@media(max-width: $screen-xs-max) {
width: 100%;
max-width: none;
}
.new-project-item-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.new-project-item-select-button {
width: 32px;
}
}
.new-project-item-select-button .fa-caret-down {
margin-left: 2px;
}
.layout-nav {
width: 100%;
background: $gray-light;
......
......@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default;
$pre-border-color: $border-color;
$table-bg-accent: $gray-light;
$zindex-popover: 900;
......@@ -88,6 +88,7 @@ $indigo-950: #1a1a40;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
......@@ -211,7 +212,6 @@ $issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54;
$ldap-members-override-bg: $orange-50;
/*
* Common component specific colors
*/
......@@ -321,6 +321,12 @@ $btn-white-active: #848484;
$badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary;
/*
* Status icons
*/
$status-icon-size: 22px;
$status-icon-margin: $gl-btn-padding;
/*
* Award emoji
*/
......@@ -634,6 +640,13 @@ $color-high-score: $green-400;
$color-average-score: $orange-400;
$color-low-score: $red-400;
/*
Repo editor
*/
$repo-editor-grey: #f6f7f9;
$repo-editor-grey-darker: #e9ebee;
$repo-editor-linear-gradient: linear-gradient(to right, $repo-editor-grey 0%, $repo-editor-grey-darker, 20%, $repo-editor-grey 40%, $repo-editor-grey 100%);
/*
Performance Bar
*/
......@@ -645,3 +658,11 @@ $perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
/*
Project Templates Icons
*/
$rails: #c00;
$node: #353535;
$java: #70ad51;
......@@ -8,14 +8,16 @@ $active-color: $indigo-700;
$active-hover-background: $active-background;
$active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08);
$hover-background: $indigo-700;
$hover-color: $white-light;
$hover-background: $white-light;
$hover-color: $gl-text-color;
$inactive-color: $gl-text-color-secondary;
$new-sidebar-width: 220px;
$new-sidebar-collapsed-width: 50px;
.page-with-new-sidebar {
padding-left: $new-sidebar-collapsed-width;
@media (min-width: $screen-md-min) {
padding-left: $new-sidebar-collapsed-width;
}
@media (min-width: $screen-lg-min) {
padding-left: $new-sidebar-width;
......@@ -40,6 +42,7 @@ $new-sidebar-collapsed-width: 50px;
.context-header {
position: relative;
margin-right: 2px;
a {
border-bottom: 1px solid $border-color;
......@@ -48,26 +51,16 @@ $new-sidebar-collapsed-width: 50px;
align-items: center;
padding: 10px 16px 10px 10px;
color: $gl-text-color;
}
@media (max-width: $screen-xs-max) {
padding-right: 30px;
}
&:hover {
background-color: $hover-background;
color: $hover-color;
border-color: $hover-background;
.avatar-container {
border-color: transparent;
}
.settings-avatar {
background-color: $indigo-500;
&:hover,
a:hover {
background-color: $hover-background;
color: $hover-color;
i {
color: $hover-color;
}
.settings-avatar {
i {
color: $hover-color;
}
}
}
......@@ -82,32 +75,6 @@ $new-sidebar-collapsed-width: 50px;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
.close-nav-button {
color: $white-light;
}
}
.close-nav-button {
display: none;
position: absolute;
top: 0;
right: 0;
height: 100%;
background-color: transparent;
border: 0;
padding: 0 10px;
@media (max-width: $screen-xs-max) {
display: block;
}
&:hover {
color: $gl-text-color;
}
}
}
.settings-avatar {
......@@ -137,11 +104,14 @@ $new-sidebar-collapsed-width: 50px;
&.sidebar-icons-only {
width: $new-sidebar-collapsed-width;
.nav-item-name,
.badge,
.project-title {
display: none;
}
.nav-item-name {
opacity: 0;
}
}
&.nav-sidebar-expanded {
......@@ -215,7 +185,7 @@ $new-sidebar-collapsed-width: 50px;
> li {
a {
padding: 8px 16px 8px 50px;
padding: 8px 16px 8px 40px;
&:hover,
&:focus {
......@@ -251,10 +221,9 @@ $new-sidebar-collapsed-width: 50px;
}
}
&:not(.active) {
&.is-showing-fly-out {
> a {
margin-left: 1px;
margin-right: 3px;
margin-right: 2px;
}
.sidebar-sub-level-items {
......@@ -262,7 +231,7 @@ $new-sidebar-collapsed-width: 50px;
position: fixed;
top: 0;
left: $new-sidebar-width;
width: 150px;
min-width: 150px;
margin-top: -1px;
padding: 8px 1px;
background-color: $white-light;
......@@ -304,6 +273,14 @@ $new-sidebar-collapsed-width: 50px;
}
}
> .active {
box-shadow: none;
> a {
background-color: transparent;
}
}
a {
padding: 8px 16px;
color: $gl-text-color;
......@@ -327,6 +304,7 @@ $new-sidebar-collapsed-width: 50px;
> a {
margin-left: 4px;
padding-left: 12px;
}
.badge {
......@@ -350,21 +328,19 @@ $new-sidebar-collapsed-width: 50px;
// Collapsed nav
.toggle-sidebar-button {
.toggle-sidebar-button,
.close-nav-button {
width: $new-sidebar-width - 2px;
position: fixed;
bottom: 0;
padding: 16px;
background-color: $gray-normal;
border: 0;
border-top: 2px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
@media (max-width: $screen-xs-max) {
display: none;
}
i {
font-size: 20px;
margin-right: 8px;
......@@ -380,9 +356,16 @@ $new-sidebar-collapsed-width: 50px;
}
}
.toggle-sidebar-button {
@media (max-width: $screen-xs-max) {
display: none;
}
}
.sidebar-icons-only {
.context-header {
height: 60px;
height: 61px;
a {
padding: 10px 4px;
......@@ -411,6 +394,7 @@ $new-sidebar-collapsed-width: 50px;
.toggle-sidebar-button {
width: $new-sidebar-collapsed-width - 2px;
padding: 16px 18px;
.collapse-text,
.fa-angle-double-left {
......@@ -426,6 +410,10 @@ $new-sidebar-collapsed-width: 50px;
// Mobile nav
.close-nav-button {
display: none;
}
.toggle-mobile-nav {
display: none;
background-color: transparent;
......@@ -445,6 +433,12 @@ $new-sidebar-collapsed-width: 50px;
}
}
@media (max-width: $screen-xs-max) {
.close-nav-button {
display: flex;
}
}
.mobile-overlay {
display: none;
......
......@@ -195,6 +195,7 @@
.board-title {
padding-top: ($gl-padding - 3px);
padding-bottom: $gl-padding;
}
}
}
......@@ -208,6 +209,7 @@
position: relative;
margin: 0;
padding: $gl-padding;
padding-bottom: ($gl-padding + 3px);
font-size: 1em;
border-bottom: 1px solid $border-color;
}
......
......@@ -3,9 +3,6 @@
*
*/
$status-icon-size: 22px;
$status-icon-margin: $gl-btn-padding;
.space-children {
@include clearfix;
......@@ -27,7 +24,7 @@ $status-icon-margin: $gl-btn-padding;
.mr-widget-heading,
.mr-widget-section,
.mr-widget-footer {
padding: 16px;
padding: $gl-padding;
border-top: solid 1px $border-color;
}
......@@ -48,7 +45,7 @@ $status-icon-margin: $gl-btn-padding;
}
.btn {
font-size: 14px;
font-size: $gl-font-size;
&[disabled] {
opacity: 0.3;
......@@ -61,8 +58,6 @@ $status-icon-margin: $gl-btn-padding;
}
&.dropdown-toggle {
padding: 4px 10px;
.fa {
color: inherit;
}
......@@ -203,7 +198,6 @@ $status-icon-margin: $gl-btn-padding;
color: $gl-text-color;
font-weight: 600;
overflow: hidden;
margin: 0 3px;
word-break: break-all;
&.label-truncated {
......@@ -285,15 +279,8 @@ $status-icon-margin: $gl-btn-padding;
padding: 5px;
}
.merge-opt-icon,
.merge-opt-title {
display: inline-block;
float: left;
}
.merge-opt-icon svg {
height: 15px;
width: 15px;
.merge-opt-icon {
line-height: 1.5;
}
.merge-opt-title {
......@@ -488,18 +475,13 @@ $status-icon-margin: $gl-btn-padding;
}
.mr-source-target {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
background-color: $gray-light;
border-radius: $border-radius-default $border-radius-default 0 0;
padding: $gl-padding-top $gl-padding;
.pull-right {
margin-top: -2px;
}
.btn-clipboard {
margin-left: 0;
padding-left: 0;
}
padding: $gl-padding / 2 $gl-padding;
.dropdown-toggle .fa {
color: $gl-text-color;
......@@ -725,90 +707,9 @@ $status-icon-margin: $gl-btn-padding;
}
}
#merge-request-widget-app .loading {
padding-top: 5px;
border-top: 1px solid $well-inner-border;
}
.approvals-footer {
display: flex;
.approvers-prefix,
.approvers-list {
display: flex;
align-items: center;
margin-right: 5px;
}
.unapprove-btn {
border: none;
background: transparent;
cursor: pointer;
&:hover {
color: $gl-text-color-secondary;
text-decoration: none;
}
&:focus {
outline: none;
}
}
.approver-avatar {
position: relative;
}
}
.link-to-member-avatar {
.disabled {
pointer-events: none;
cursor: default;
}
.avatar {
margin-bottom: 0;
margin-left: 7px;
display: block;
}
}
.mr-memory-usage {
p.usage-info-loading .usage-info-load-spinner {
margin-right: 10px;
font-size: 16px;
}
}
.mr-widget-icon {
font-size: 22px;
margin: 0 10px 0 0;
}
.mr-widget-code-quality {
padding-top: $gl-padding-top;
.code-quality-container {
border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
margin: 4px -16px 0;
.mr-widget-code-quality-list {
list-style: none;
padding: 4px 36px;
margin: 0;
line-height: $code_line_height;
li.success {
color: $green-500;
}
li.failed {
color: $red-500;
}
}
}
}
......@@ -7,7 +7,8 @@
}
.new_project,
.edit-project {
.edit-project,
.import-project {
.sharing-and-permissions {
.header {
......@@ -465,6 +466,7 @@ a.deploy-project-label {
}
}
.project-template,
.project-import {
.form-group {
margin-bottom: 5px;
......@@ -479,7 +481,44 @@ a.deploy-project-label {
.btn {
padding: 8px;
margin-left: 10px;
margin-right: 10px;
}
.blank-option {
min-width: 70px;
}
.btn-template-icon {
height: 24px;
width: inherit;
display: block;
margin: 0 auto 4px;
font-size: 24px;
@media (min-width: $screen-xs-max) {
top: 0;
}
}
@media (max-width: $screen-xs-max) {
.btn-template-icon {
display: inline-block;
height: 14px;
font-size: 14px;
margin: 0;
}
}
.icon-rails path {
fill: $rails;
}
.icon-node-express path {
fill: $node;
}
.icon-java-spring path {
fill: $java;
}
> div {
......@@ -489,6 +528,97 @@ a.deploy-project-label {
}
}
.project-templates-buttons .btn:last-child {
margin-right: 0;
}
.create-project-options {
display: flex;
@media (max-width: $screen-xs-max) {
display: block;
}
.first-column {
@media(min-width: $screen-xs-min) {
max-width: 50%;
padding-right: 30px;
}
@media(max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
}
}
.second-column {
@media(min-width: $screen-xs-min) {
width: 50%;
flex: 1;
padding-left: 30px;
position: relative;
}
@media(max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
padding-left: 0;
position: relative;
}
// Mobile
@media (max-width: $screen-xs-max) {
padding-top: 30px;
}
&::before {
content: "OR";
position: absolute;
left: 0;
top: 40%;
z-index: 10;
padding: 8px 0;
text-align: center;
background-color: $white-light;
color: $gl-text-color-tertiary;
transform: translateX(-50%);
font-size: 12px;
font-weight: bold;
line-height: 20px;
// Mobile
@media (max-width: $screen-xs-max) {
left: 50%;
top: 10px;
transform: translateY(-50%);
padding: 0 8px;
}
}
&::after {
content: "";
position: absolute;
background-color: $border-color;
bottom: 0;
left: 0;
right: auto;
height: 100%;
width: 1px;
top: 0;
// Mobile
@media (max-width: $screen-xs-max) {
top: 10px;
left: 10px;
right: 10px;
height: 1px;
width: auto;
}
}
}
}
.project-stats {
font-size: 0;
text-align: center;
......
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.monaco-loader {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: $black-transparent;
}
.modal.popup-dialog {
display: block;
background-color: $black-transparent;
z-index: 2100;
@media (min-width: $screen-md-min) {
.modal-dialog {
width: 600px;
margin: 30px auto;
}
}
}
.project-refs-form,
.project-refs-target-form {
display: inline-block;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.commit-message {
@include str-truncated(250px);
}
.editable-mode {
display: inline-block;
}
.blob-viewer[data-type="rich"] {
margin: 20px;
}
.repository-view.tree-content-holder {
border: 1px solid $border-color;
border-radius: $border-radius-default;
color: $almost-black;
.panel-right {
display: inline-block;
width: 80%;
.monaco-editor.vs {
.line-numbers {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.cursor {
display: none !important;
}
}
&.edit-mode {
.blob-viewer-container {
overflow: hidden;
}
.monaco-editor.vs {
.cursor {
background: $black;
border-color: $black;
display: block !important;
}
}
}
.blob-viewer-container {
height: calc(100vh - 63px);
overflow: auto;
}
#tabs {
padding-left: 0;
margin-bottom: 0;
display: flex;
white-space: nowrap;
width: 100%;
overflow-y: hidden;
overflow-x: auto;
li {
animation: swipeRightAppear ease-in 0.1s;
animation-iteration-count: 1;
transform-origin: 0% 50%;
list-style-type: none;
background: $gray-normal;
display: inline-block;
padding: 10px 18px;
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
white-space: nowrap;
&.remove {
animation: swipeRightDissapear ease-in 0.1s;
animation-iteration-count: 1;
transform-origin: 0% 50%;
a {
width: 0;
}
}
&.active {
background: $white-light;
border-bottom: none;
}
a {
@include str-truncated(100px);
color: $black;
display: inline-block;
width: 100px;
text-align: center;
vertical-align: middle;
&.close {
width: auto;
font-size: 15px;
opacity: 1;
margin-right: -6px;
}
}
i.fa.fa-times,
i.fa.fa-circle {
float: right;
margin-top: 3px;
margin-left: 15px;
color: $gray-darkest;
}
i.fa.fa-circle {
color: $brand-success;
}
&.tabs-divider {
width: 100%;
background-color: $white-light;
border-right: none;
border-top-right-radius: 2px;
}
}
}
#repo-file-buttons {
background-color: $white-light;
border-bottom: 1px solid $white-normal;
padding: 5px 10px;
position: relative;
border-top: 1px solid $white-normal;
margin-top: -5px;
}
#binary-viewer {
height: 80vh;
overflow: auto;
margin: 0;
.blob-viewer {
padding-top: 20px;
padding-left: 20px;
}
.binary-unknown {
text-align: center;
padding-top: 100px;
background: $gray-light;
height: 100%;
font-size: 17px;
span {
display: block;
}
}
}
}
#commit-area {
background: $gray-light;
padding: 20px;
span.help-block {
padding-top: 7px;
margin-top: 0;
}
}
#view-toggler {
height: 41px;
position: relative;
display: block;
border-bottom: 1px solid $white-normal;
background: $white-light;
margin-top: -5px;
}
#binary-viewer {
img {
max-width: 100%;
}
}
#sidebar {
&.sidebar-mini {
display: inline-block;
vertical-align: top;
width: 20%;
border-right: 1px solid $white-normal;
height: calc(100vh + 20px);
overflow: auto;
}
table {
margin-bottom: 0;
}
tr {
animation: fadein 0.5s;
cursor: pointer;
&.repo-file-options td {
padding: 0;
border-top: none;
background: $gray-light;
width: 100%;
display: inline-block;
&:first-child {
border-top-left-radius: 2px;
}
.title {
display: inline-block;
font-size: 10px;
text-transform: uppercase;
font-weight: bold;
color: $gray-darkest;
width: 185px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
padding: 2px 16px;
}
}
.fa {
margin-right: 5px;
}
td {
white-space: nowrap;
}
}
a {
color: $almost-black;
display: inline-block;
vertical-align: middle;
}
ul {
list-style-type: none;
padding: 0;
li {
border-bottom: 1px solid $border-gray-normal;
padding: 10px 20px;
a {
color: $almost-black;
}
.fa {
font-size: $code_font_size;
margin-right: 5px;
}
}
}
}
}
.animation-container {
background: $repo-editor-grey;
height: 40px;
overflow: hidden;
position: relative;
&.animation-container-small {
height: 12px;
}
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative;
}
div {
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.line-of-code-1 {
left: 0;
top: 8px;
}
.line-of-code-2 {
left: 150px;
top: 0;
height: 10px;
}
.line-of-code-3 {
left: 0;
top: 23px;
}
.line-of-code-4 {
left: 0;
top: 38px;
}
.line-of-code-5 {
left: 200px;
top: 28px;
height: 10px;
}
.line-of-code-6 {
top: 14px;
left: 230px;
height: 10px;
}
}
.render-error {
min-height: calc(100vh - 63px);
p {
width: 100%;
}
}
@keyframes blockTextShine {
0% {
transform: translateX(-468px);
}
100% {
transform: translateX(468px);
}
}
@keyframes swipeRightAppear {
0% {
transform: scaleX(0.00);
}
100% {
transform: scaleX(1.00);
}
}
@keyframes swipeRightDissapear {
0% {
transform: scaleX(1.00);
}
100% {
transform: scaleX(0.00);
}
}
......@@ -87,7 +87,7 @@
}
.add-to-tree {
vertical-align: top;
vertical-align: middle;
padding: 6px 10px;
}
......@@ -216,6 +216,9 @@
}
.blob-upload-dropzone-previews {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
border: 2px;
border-style: dashed;
......
module RendersBlob
extend ActiveSupport::Concern
def render_blob_json(blob)
def blob_json(blob)
viewer =
case params[:viewer]
when 'rich'
......@@ -11,13 +11,21 @@ module RendersBlob
else
blob.simple_viewer
end
return render_404 unless viewer
render json: {
return unless viewer
{
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_async: false)
}
end
def render_blob_json(blob)
json = blob_json(blob)
return render_404 unless json
render json: json
end
def conditionally_expand_blob(blob)
blob.expand! if params[:expanded] == 'true'
end
......
......@@ -45,8 +45,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_projects(finder_params)
ProjectsFinder.new(params: finder_params, current_user: current_user)
.execute.includes(:route, namespace: :route)
ProjectsFinder
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: :route)
end
def load_events
......
......@@ -12,15 +12,7 @@ class Import::GitlabProjectsController < Import::BaseController
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename)
FileUtils.mkdir_p(File.dirname(import_upload_path))
FileUtils.copy_entry(project_params[:file].path, import_upload_path)
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
import_upload_path,
project_params[:path]).execute
@project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute
if @project.saved?
redirect_to(
......
......@@ -37,16 +37,11 @@ class Projects::BlobController < Projects::ApplicationController
respond_to do |format|
format.html do
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
render 'show'
show_html
end
format.json do
render_blob_json(@blob)
show_json
end
end
end
......@@ -190,4 +185,34 @@ class Projects::BlobController < Projects::ApplicationController
@last_commit_sha = Gitlab::Git::Commit
.last_for_path(@repository, @ref, @path).sha
end
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
render 'show'
end
def show_json
json = blob_json(@blob)
return render_404 unless json
render json: json.merge(
path: blob.path,
name: blob.name,
extension: blob.extension,
size: blob.raw_size,
mime_type: blob.mime_type,
binary: blob.raw_binary?,
simple_viewer: blob.simple_viewer&.class&.partial_name,
rich_viewer: blob.rich_viewer&.class&.partial_name,
show_viewer_switcher: !!blob.show_viewer_switcher?,
render_error: blob.simple_viewer&.render_error || blob.rich_viewer&.render_error,
raw_path: project_raw_path(project, @id),
blame_path: project_blame_path(project, @id),
commits_path: project_commits_path(project, @id),
permalink: project_blob_path(project, File.join(@commit.id, @path))
)
end
end
......@@ -24,12 +24,19 @@ class Projects::TreeController < Projects::ApplicationController
end
end
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
respond_to do |format|
format.html
# Disable cache so browser history works
format.js { no_cache_headers }
format.html do
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
end
format.js do
# Disable cache so browser history works
no_cache_headers
end
format.json do
render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree)
end
end
end
......
......@@ -222,21 +222,34 @@ class ProjectsController < Projects::ApplicationController
end
def refs
branches = BranchesFinder.new(@repository, params).execute.map(&:name)
find_refs = params['find']
options = {
s_('RefSwitcher|Branches') => branches.take(100)
}
find_branches = true
find_tags = true
find_commits = true
unless find_refs.nil?
find_branches = find_refs.include?('branches')
find_tags = find_refs.include?('tags')
find_commits = find_refs.include?('commits')
end
options = {}
if find_branches
branches = BranchesFinder.new(@repository, params).execute.take(100).map(&:name)
options[s_('RefSwitcher|Branches')] = branches
end
unless @repository.tag_count.zero?
tags = TagsFinder.new(@repository, params).execute.map(&:name)
if find_tags && @repository.tag_count.nonzero?
tags = TagsFinder.new(@repository, params).execute.take(100).map(&:name)
options[s_('RefSwitcher|Tags')] = tags.take(100)
options[s_('RefSwitcher|Tags')] = tags
end
# If reference is commit id - we should add it to branch/tag selectbox
ref = Addressable::URI.unescape(params[:ref])
if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
if find_commits && ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
options['Commits'] = [ref]
end
......@@ -326,6 +339,7 @@ class ProjectsController < Projects::ApplicationController
:runners_token,
:tag_list,
:visibility_level,
:template_name,
project_feature_attributes: %i[
builds_access_level
......
......@@ -309,4 +309,12 @@ module ApplicationHelper
def show_new_nav?
cookies["new_nav"] == "true"
end
def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true"
end
def show_new_repo?
cookies["new_repo"] == "true" && body_data_page != 'projects:show'
end
end
......@@ -19,7 +19,8 @@ module AvatarsHelper
class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes
data: data_attributes,
lazy: true
)
end
......
......@@ -118,7 +118,7 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
def blob_raw_url
def blob_raw_path
if @build && @entry
raw_project_job_artifacts_path(@project, @build, path: @entry.path)
elsif @snippet
......@@ -235,7 +235,7 @@ module BlobHelper
title = 'Open raw'
end
link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
link_to icon, blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def blob_render_error_reason(viewer)
......@@ -270,7 +270,7 @@ module BlobHelper
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end
options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
options << link_to('download it', blob_raw_path, target: '_blank', rel: 'noopener noreferrer')
options
end
......
......@@ -88,15 +88,15 @@ module DiffHelper
end
def submodule_link(blob, ref, repository = @repository)
tree, commit = submodule_links(blob, ref, repository)
commit_id = if commit.nil?
project_url, tree_url = submodule_links(blob, ref, repository)
commit_id = if tree_url.nil?
Commit.truncate_sha(blob.id)
else
link_to Commit.truncate_sha(blob.id), commit
link_to Commit.truncate_sha(blob.id), tree_url
end
[
content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
content_tag(:span, link_to(truncate(blob.name, length: 40), project_url)),
'@',
content_tag(:span, commit_id, class: 'commit-sha')
].join(' ').html_safe
......
......@@ -57,11 +57,11 @@ module DropdownsHelper
output.html_safe
end
def dropdown_title(title, back: false)
def dropdown_title(title, options: {})
content_tag :div, class: "dropdown-title" do
title_output = ""
if back
if options.fetch(:back, false)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
icon('arrow-left')
end
......@@ -69,14 +69,25 @@ module DropdownsHelper
title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times', class: 'dropdown-menu-close-icon')
if options.fetch(:close, true)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times', class: 'dropdown-menu-close-icon')
end
end
title_output.html_safe
end
end
def dropdown_input(placeholder, input_id: nil)
content_tag :div, class: "dropdown-input" do
filter_output = text_field_tag input_id, nil, class: "dropdown-input-field dropdown-no-filter", placeholder: placeholder, autocomplete: 'off'
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
filter_output.html_safe
end
end
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
......
module IconsHelper
extend self
include FontAwesome::Rails::IconHelper
# Creates an icon tag given icon name(s) and possible icon modifiers.
......
......@@ -2,6 +2,7 @@ module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar
class_name
end
......
......@@ -225,6 +225,26 @@ module ProjectsHelper
end
end
# Returns true if any projects are present.
#
# If the relation has a LIMIT applied we'll cast the relation to an Array
# since repeated any? checks would otherwise result in multiple COUNT queries
# being executed.
#
# If no limit is applied we'll just issue a COUNT since the result set could
# be too large to load into memory.
def any_projects?(projects)
if projects.limit_value
projects.to_a.any?
else
projects.except(:offset).any?
end
end
def has_projects_or_name?(projects, params)
!!(params[:name] || any_projects?(projects))
end
private
def repo_children_classes(field)
......
module SubmoduleHelper
include Gitlab::ShellAdapter
extend self
VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
......@@ -59,7 +59,7 @@ module SubmoduleHelper
return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
project].join('')
url_with_dotgit = url_no_dotgit + '.git'
url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
url_with_dotgit == Gitlab::Shell.new.url_to_repo([namespace, '/', project].join(''))
end
def relative_self_url?(url)
......
......@@ -82,7 +82,7 @@ module BlobViewer
# format of the blob.
#
# Prefer to implement a client-side viewer, where the JS component loads the
# binary from `blob_raw_url` and does its own format validation and error
# binary from `blob_raw_path` and does its own format validation and error
# rendering, especially for potentially large binary formats.
def render_error
if too_large?
......
......@@ -17,7 +17,7 @@ module BlobViewer
# build artifacts, can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from
# `blob_raw_url` using AJAX.
# `blob_raw_path` using AJAX.
return :server_side_but_stored_externally if blob.stored_externally?
super
......
......@@ -16,6 +16,7 @@ module Issuable
include TimeTrackable
include Importable
include Editable
include AfterCommitQueue
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
......
......@@ -78,6 +78,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
alias_attribute :title, :name
......@@ -939,7 +940,7 @@ class Project < ActiveRecord::Base
end
def repo
repository.raw
repository.rugged
end
def url_to_repo
......@@ -1045,13 +1046,18 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
repository.before_change_head
repository.rugged.references.create('HEAD',
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
repository.after_change_head
reload_default_branch
if repository.branch_exists?(branch)
repository.before_change_head
repository.rugged.references.create('HEAD',
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
repository.after_change_head
reload_default_branch
else
errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist")
false
end
end
def forked_from?(project)
......
......@@ -124,10 +124,10 @@ class ProjectWiki
return false
end
def update_page(page, content, format = :markdown, message = nil)
def update_page(page, content:, title: nil, format: :markdown, message: nil)
commit = commit_details(:updated, message, page.title)
wiki.update_page(page, page.name, format.to_sym, content, commit)
wiki.update_page(page, title || page.name, format.to_sym, content, commit)
update_elastic_index
......
......@@ -139,17 +139,13 @@ class Repository
return []
end
ref ||= root_ref
args = %W(
log #{ref} --pretty=%H --skip #{offset}
--max-count #{limit} --grep=#{query} --regexp-ignore-case
)
args = args.concat(%W(-- #{path})) if path.present?
git_log_results = run_git(args).first.lines
git_log_results.map { |c| commit(c.chomp) }.compact
raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
if is_enabled
find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
else
find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
end
end
end
def find_branch(name, fresh_repo: true)
......@@ -1266,4 +1262,25 @@ class Repository
def circuit_breaker
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage)
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref
args = %W(
log #{ref} --pretty=%H --skip #{offset}
--max-count #{limit} --grep=#{query} --regexp-ignore-case
)
args = args.concat(%W(-- #{path})) if path.present?
git_log_results = run_git(args).first.lines
git_log_results.map { |c| commit(c.chomp) }.compact
end
def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
raw_repository
.gitaly_commit_client
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
end
end
......@@ -659,7 +659,11 @@ class User < ActiveRecord::Base
end
def projects_limit_left
projects_limit - personal_projects.count
projects_limit - personal_projects_count
end
def personal_projects_count
@personal_projects_count ||= personal_projects.count
end
def projects_limit_percent
......@@ -673,16 +677,14 @@ class User < ActiveRecord::Base
events = events.where(project_id: project_ids) if project_ids
# Use the latest event that has not been pushed or merged recently
events.recent.find do |event|
project = Project.find_by_id(event.project_id)
next unless project
if project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
.where(source_project_id: project.id,
source_branch: event.branch_name)
merge_requests.empty?
end
events.includes(:project).recent.find do |event|
next unless event.project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
.where(source_project_id: event.project.id,
source_branch: event.branch_name)
merge_requests.empty?
end
end
......@@ -850,7 +852,7 @@ class User < ActiveRecord::Base
{
name: name,
username: username,
avatar_url: avatar_url
avatar_url: avatar_url(only_path: false)
}
end
......
......@@ -180,31 +180,50 @@ class WikiPage
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def create(attr = {})
@attributes.merge!(attr)
def create(attrs = {})
@attributes.merge!(attrs)
save :create_page, title, content, format, message
save(page_details: title) do
wiki.create_page(title, content, format, message)
end
end
# Updates an existing Wiki Page, creating a new version.
#
# new_content - The raw markup content to replace the existing.
# format - Optional symbol representing the content format.
# See ProjectWiki::MARKUPS Hash for available formats.
# message - Optional commit message to set on the new version.
# last_commit_sha - Optional last commit sha to validate the page unchanged.
# attrs - Hash of attributes to be updated on the page.
# :content - The raw markup content to replace the existing.
# :format - Optional symbol representing the content format.
# See ProjectWiki::MARKUPS Hash for available formats.
# :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
# :title - The Title to replace existing title
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def update(new_content, format: :markdown, message: nil, last_commit_sha: nil)
@attributes[:content] = new_content
@attributes[:format] = format
def update(attrs = {})
last_commit_sha = attrs.delete(:last_commit_sha)
if last_commit_sha && last_commit_sha != self.last_commit_sha
raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
end
save :update_page, @page, content, format, message
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
page_details =
if title.present? && @page.title != title
title
else
@page.url_path
end
save(page_details: page_details) do
wiki.update_page(
@page,
content: content,
format: format,
message: attrs[:message],
title: title
)
end
end
# Destroys the Wiki Page.
......@@ -236,30 +255,19 @@ class WikiPage
attributes[:format] = @page.format
end
def save(method, *args)
saved = false
def save(page_details:)
return unless valid?
project_wiki = wiki
if valid? && project_wiki.send(method, *args)
page_details = if method == :update_page
# Use url_path instead of path to omit format extension
@page.url_path
else
title
end
page_title, page_dir = project_wiki.page_title_and_dir(page_details)
gollum_wiki = project_wiki.wiki
@page = gollum_wiki.paged(page_title, page_dir)
unless yield
errors.add(:base, wiki.error_message)
return false
end
set_attributes
page_title, page_dir = wiki.page_title_and_dir(page_details)
gollum_wiki = wiki.wiki
@page = gollum_wiki.paged(page_title, page_dir)
@persisted = true
saved = true
else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message
end
saved
set_attributes
@persisted = errors.blank?
end
end
class BlobEntity < Grape::Entity
include RequestAwareEntity
expose :id, :path, :name, :mode
expose :last_commit do |blob|
request.project.repository.last_commit_for_path(blob.commit_id, blob.path)
end
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
expose :url do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path))
end
end
class SubmoduleEntity < Grape::Entity
include RequestAwareEntity
expose :id, :path, :name, :mode
expose :icon do |blob|
'archive'
end
expose :project_url do |blob|
submodule_links(blob, request).first
end
expose :tree_url do |blob|
submodule_links(blob, request).last
end
private
def submodule_links(blob, request)
@submodule_links ||= SubmoduleHelper.submodule_links(blob, request.ref, request.repository)
end
end
class TreeEntity < Grape::Entity
include RequestAwareEntity
expose :id, :path, :name, :mode
expose :last_commit do |tree|
request.project.repository.last_commit_for_path(tree.commit_id, tree.path)
end
expose :icon do |tree|
IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)
end
expose :url do |tree|
project_tree_path(request.project, File.join(request.ref, tree.path))
end
end
# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
class TreeRootEntity < Grape::Entity
expose :path
expose :trees, using: TreeEntity
expose :blobs, using: BlobEntity
expose :submodules, using: SubmoduleEntity
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment