Commit 8181f58d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into auto-pipelines-vue

* master: (35 commits)
  Adds back removed class in merge request pipelines table
  Fix dropdown icon alignment
  Simplify HTML of mini pipeline graph and dropdown Creates individual html for dropdown Adds simplified  CSS for the new dropdown Removes old CSS Improves dropdown item in Chrome, Firefox and Safari Use SCSS variables for colors. Fix scss linter errors Adds animation when the stage is hovered. Adds back tooltip on dropdown toggle Fixes broken tests additional css changes to get more into direction of mockups
  Adds CHANGELOG entry
  Removes unneeded `window` declaration
  Decreases font-size on login page
  Ensure internal Gitlab::Git references use the namespace
  Absorb gitlab_git
  Fix review comments.
  Add spec for note edit and fix one commented spec.
  Review fixes.
  Use gl.utils.isInViewport and improve gl.utils.animateToElement.
  Make sure elements share the same scope
  Hide edit warning element when form reverted.
  Remove unnecessary styling came from merge conflicts.
  Fix single note edit form specs.
  Separate edit form in Changes and Discussions tab.
  Fix warning styling for responsive design.
  Fix task list for single edit note widget changes.
  Fix notes spec.
  ...
parents e4da8b11 cd85baf5
......@@ -16,6 +16,8 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
......@@ -49,10 +51,6 @@ gem 'u2f', '~> 0.2.1'
# Browser detection
gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.7.0'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
......
......@@ -255,11 +255,6 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.0)
gitlab_git (10.7.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
rugged (~> 0.24.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
......@@ -857,7 +852,6 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.0)
gitlab_git (~> 10.7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
......@@ -942,6 +936,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
......@@ -988,4 +983,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.13.6
1.13.7
......@@ -45,7 +45,7 @@
return fn(item);
}).filter(Boolean);
window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: {
store: {
type: Object,
......@@ -55,7 +55,7 @@
},
components: {
'environment-item': window.gl.environmentsList.EnvironmentItem,
'environment-item': gl.environmentsList.EnvironmentItem,
},
data() {
......
......@@ -5,7 +5,7 @@
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: {
actions: {
type: Array,
......
......@@ -5,7 +5,7 @@
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: {
externalUrl: {
type: String,
......
......@@ -29,12 +29,12 @@
gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
components: {
'commit-component': window.gl.CommitComponent,
'actions-component': window.gl.environmentsList.ActionsComponent,
'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
'stop-component': window.gl.environmentsList.StopComponent,
'rollback-component': window.gl.environmentsList.RollbackComponent,
'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent,
'commit-component': gl.CommitComponent,
'actions-component': gl.environmentsList.ActionsComponent,
'external-url-component': gl.environmentsList.ExternalUrlComponent,
'stop-component': gl.environmentsList.StopComponent,
'rollback-component': gl.environmentsList.RollbackComponent,
'terminal-button-component': gl.environmentsList.TerminalButtonComponent,
},
props: {
......@@ -183,7 +183,7 @@
* @returns {String}
*/
createdDate() {
return window.gl.environmentsList.timeagoInstance.format(
return gl.environmentsList.timeagoInstance.format(
this.model.last_deployment.deployable.created_at,
);
},
......
......@@ -5,7 +5,7 @@
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: {
retryUrl: {
type: String,
......
......@@ -5,7 +5,7 @@
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: {
stopUrl: {
type: String,
......
......@@ -5,7 +5,7 @@
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
props: {
terminalPath: {
type: String,
......
......@@ -7,15 +7,17 @@
$(() => {
window.gl = window.gl || {};
if (window.gl.EnvironmentsListApp) {
window.gl.EnvironmentsListApp.$destroy(true);
if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true);
}
const Store = window.gl.environmentsList.EnvironmentsStore;
const Store = gl.environmentsList.EnvironmentsStore;
window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({
gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
propsData: {
store: Store.create(),
},
});
});
......@@ -35,8 +35,8 @@
autosize(this.textarea);
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
}
gl.text.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
return this.form.show();
......
......@@ -106,8 +106,9 @@
);
};
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
gl.utils.getPagePath = function(index) {
index = index || 0;
return $('body').data('page').split(':')[index];
};
gl.utils.parseUrl = function (url) {
......@@ -127,6 +128,17 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
gl.utils.scrollToElement = function($el) {
var top = $el.offset().top;
gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
return $('body, html').animate({
scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight)
}, 200);
};
})(window);
}).call(this);
/**
* CustomEvent support for IE
*/
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(e, params) {
const options = params || { bubbles: false, cancelable: false, detail: undefined };
const evt = document.createEvent('CustomEvent');
evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail);
return evt;
};
window.CustomEvent.prototype = window.Event.prototype;
}
......@@ -52,6 +52,12 @@
this.setupMainTargetNoteForm();
this.initTaskList();
this.collapseLongCommitList();
// We are in the Merge Requests page so we need another edit form for Changes tab
if (gl.utils.getPagePath(1) === 'merge_requests') {
$('.note-edit-form').clone()
.addClass('mr-note-edit-form').insertAfter('.note-edit-form');
}
}
Notes.prototype.addBinding = function() {
......@@ -63,7 +69,7 @@
// change note in UI after update
$(document).on("ajax:success", "form.edit-note", this.updateNote);
// Edit note link
$(document).on("click", ".js-note-edit", this.showEditForm);
$(document).on("click", ".js-note-edit", this.showEditForm.bind(this));
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
$(document).on("click", ".js-comment-button", this.updateCloseButton);
......@@ -466,6 +472,7 @@
var $html, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
this.revertNoteEditForm();
gl.utils.localTimeAgo($('.js-timeago', $html));
$html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
......@@ -480,48 +487,56 @@
};
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
var isAllowed = true;
if (currentContent === initialContent) {
this.removeNoteEditForm($el);
}
else {
var $buttons = $el.find('.note-form-actions');
var isWidgetVisible = gl.utils.isInViewport($el.get(0));
if (!isWidgetVisible) {
gl.utils.scrollToElement($el);
}
$el.find('.js-edit-warning').show();
isAllowed = false;
}
return isAllowed;
}
/*
Called in response to clicking the edit note link
Replaces the note text with the note edit form
Adds a data attribute to the form with the original content of the note for cancellations
*/
Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) {
var $noteText, done, form, note;
e.preventDefault();
note = $(this).closest(".note");
note.addClass("is-editting");
form = note.find(".note-edit-form");
form.addClass('current-note-edit-form');
// Show the attachment delete link
note.find(".js-note-attachment-delete").show();
done = function($noteText) {
var noteTextVal;
// Neat little trick to put the cursor at the end
noteTextVal = $noteText.val();
// Store the original note text in a data attribute to retrieve if a user cancels edit.
form.find('form.edit-note').data('original-note', noteTextVal);
return $noteText.val('').val(noteTextVal);
};
new GLForm(form);
if ((scrollTo != null) && (myLastNote != null)) {
// scroll to the bottom
// so the open of the last element doesn't make a jump
$('html, body').scrollTop($(document).height());
return $('html, body').animate({
scrollTop: myLastNote.offset().top - 150
}, 500, function() {
var $noteText;
$noteText = form.find(".js-note-text");
$noteText.focus();
return done($noteText);
});
} else {
$noteText = form.find('.js-note-text');
$noteText.focus();
return done($noteText);
var $target = $(e.target);
var $editForm = $(this.getEditFormSelector($target));
var $note = $target.closest('.note');
var $currentlyEditing = $('.note.is-editting:visible');
if ($currentlyEditing.length) {
var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
if (!isEditAllowed) {
return;
}
}
$note.find('.js-note-attachment-delete').show();
$editForm.addClass('current-note-edit-form');
$note.addClass('is-editting');
this.putEditFormInPlace($target);
};
......@@ -532,19 +547,41 @@
*/
Notes.prototype.cancelEdit = function(e) {
var note;
e.preventDefault();
note = $(e.target).closest('.note');
var $target = $(e.target);
var note = $target.closest('.note');
note.find('.js-edit-warning').hide();
this.revertNoteEditForm($target);
return this.removeNoteEditForm(note);
};
Notes.prototype.revertNoteEditForm = function($target) {
$target = $target || $('.note.is-editting:visible');
var selector = this.getEditFormSelector($target);
var $editForm = $(selector);
$editForm.insertBefore('.notes-form');
$editForm.find('.js-comment-button').enable();
$editForm.find('.js-edit-warning').hide();
};
Notes.prototype.getEditFormSelector = function($el) {
var selector = '.note-edit-form:not(.mr-note-edit-form)';
if ($el.parents('#diffs').length) {
selector = '.note-edit-form.mr-note-edit-form';
}
return selector;
};
Notes.prototype.removeNoteEditForm = function(note) {
var form;
form = note.find(".current-note-edit-form");
note.removeClass("is-editting");
form.removeClass("current-note-edit-form");
var form = note.find('.current-note-edit-form');
note.removeClass('is-editting');
form.removeClass('current-note-edit-form');
form.find('.js-edit-warning').hide();
// Replace markdown textarea text with original note text.
return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
......@@ -837,15 +874,44 @@
Notes.prototype.initTaskList = function() {
this.enableTaskList();
return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList);
return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this));
};
Notes.prototype.enableTaskList = function() {
return $('.note .js-task-list-container').taskList('enable');
};
Notes.prototype.updateTaskList = function() {
return $('form', this).submit();
Notes.prototype.putEditFormInPlace = function($el) {
var $editForm = $(this.getEditFormSelector($el));
var $note = $el.closest('.note');
$editForm.insertAfter($note.find('.note-text'));
var $originalContentEl = $note.find('.original-note-content');
var originalContent = $originalContentEl.text().trim();
var postUrl = $originalContentEl.data('post-url');
var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type');
new GLForm($editForm.find('form'));
$editForm.find('form').attr('action', postUrl);
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm.find('.js-note-text').focus().val(originalContent);
$editForm.find('.js-md-write-button').trigger('click');
$editForm.find('.referenced-users').hide();
}
Notes.prototype.updateTaskList = function(e) {
var $target = $(e.target);
var $list = $target.closest('.js-task-list-container');
var $editForm = $(this.getEditFormSelector($target));
var $note = $list.closest('.note');
this.putEditFormInPlace($list);
$editForm.find('#note_note').val($note.find('.original-task-list').val());
$('form', $list).submit();
};
Notes.prototype.updateNotesCount = function(updateCount) {
......
//= require xterm/encoding-indexes
//= require xterm/encoding
//= require xterm/xterm.js
//= require xterm/fit.js
//= require ./terminal.js
......
......@@ -4,8 +4,3 @@
color: $badge-color;
vertical-align: baseline;
}
.badge-dark {
background-color: $badge-bg-dark;
color: $badge-color-dark;
}
.centered-light-block {
text-align: center;
color: $gl-gray;
color: $gl-text-color;
margin: 20px;
}
.nothing-here-block {
text-align: center;
padding: 20px;
color: $gl-gray;
color: $gl-text-color;
font-weight: normal;
font-size: 14px;
line-height: 36px;
......@@ -29,7 +29,7 @@
margin-bottom: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
color: $gl-gray;
color: $gl-text-color;
&.oneline-block {
line-height: 42px;
......@@ -135,11 +135,11 @@
}
.cover-title {
color: $gl-header-color;
color: $gl-text-color;
font-size: 23px;
h1 {
color: $gl-gray-dark;
color: $gl-text-color;
margin-bottom: 6px;
font-size: 23px;
}
......@@ -153,7 +153,7 @@
p {
padding: 0 $gl-padding;
color: $gl-text-color-dark;
color: $gl-text-color;
}
}
......@@ -211,7 +211,7 @@
display: inline;
font-weight: normal;
font-size: 24px;
color: $gl-title-color;
color: $gl-text-color;
}
}
}
......
......@@ -88,7 +88,7 @@
}
@mixin btn-gray {
@include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
@include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color);
}
@mixin btn-white {
......@@ -242,7 +242,7 @@
}
.btn-transparent {
color: $gl-gray-light;
color: $gl-text-color-secondary;
background-color: transparent;
border: 0;
......@@ -338,7 +338,7 @@
margin-left: 10px;
i {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
......
......@@ -412,7 +412,7 @@ table {
padding: 0 10px;
clip: auto;
text-decoration: none;
color: $gl-title-color;
color: $gl-text-color;
background: $gray-light;
z-index: 1;
}
......
......@@ -201,7 +201,7 @@
}
.icon-play {
fill: $gl-gray-light;
fill: $gl-text-color-secondary;
margin-right: 6px;
height: 12px;
width: 11px;
......@@ -209,7 +209,7 @@
}
.dropdown-header {
color: $gl-gray-light;
color: $gl-text-color-secondary;
font-size: 13px;
line-height: 22px;
padding: 0 10px;
......@@ -222,7 +222,7 @@
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
......@@ -592,7 +592,7 @@
}
.ui-datepicker-title {
color: $gl-gray;
color: $gl-text-color;
font-size: 14px;
line-height: 1;
font-weight: normal;
......@@ -614,17 +614,17 @@
.dropdown-menu-inner-title {
display: block;
color: $gl-title-color;
color: $gl-text-color;
font-weight: 600;
}
.dropdown-menu-inner-content {
display: block;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.dropdown-toggle-text {
&.is-default {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
......@@ -153,7 +153,7 @@ label {
}
.form-control::-webkit-input-placeholder {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.input-group {
......
......@@ -45,7 +45,7 @@ header {
padding: 0;
.nav > li > a {
color: $gl-gray-light;
color: $gl-text-color-secondary;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
......@@ -63,7 +63,7 @@ header {
&:focus,
&:active {
background-color: $gray-light;
color: darken($gl-gray-light, 30%);
color: darken($gl-text-color-secondary, 30%);
.todos-pending-count {
background: darken($todo-alert-blue, 10%);
......@@ -88,7 +88,7 @@ header {
}
&.active {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
}
......
......@@ -35,10 +35,10 @@
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
color: $gl-gray;
color: $gl-text-color;
svg {
fill: $gl-gray;
fill: $gl-text-color;
}
}
......
......@@ -41,6 +41,6 @@
}
&.status-box-upcoming {
background: $gl-gray-light;
background: $gl-text-color-secondary;
}
}
......@@ -128,7 +128,7 @@ ul.content-list {
}
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
.member-group-link {
......@@ -230,7 +230,7 @@ ul.content-list {
}
.label-default {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
......
......@@ -73,7 +73,7 @@
}
.referenced-users {
color: $gl-header-color;
color: $gl-text-color;
padding-top: 10px;
}
......@@ -135,7 +135,7 @@
.toolbar-btn {
float: left;
padding: 0 5px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
background: transparent;
border: 0;
outline: 0;
......
......@@ -46,7 +46,7 @@
&.light {
a {
color: $gl-gray;
color: $gl-text-color;
}
}
}
......
......@@ -51,7 +51,7 @@
margin-bottom: -1px;
font-size: 14px;
line-height: 28px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
border-bottom: 2px solid transparent;
&:hover,
......@@ -315,7 +315,7 @@
.fa-caret-down {
margin-left: 5px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.dropdown {
......
......@@ -14,7 +14,7 @@
.header-action-buttons {
i {
color: $gl-gray-light;
color: $gl-text-color-secondary;
font-size: 13px;
margin-right: 3px;
}
......@@ -42,7 +42,7 @@
.commit-committer-link,
.commit-author-link {
color: $gl-gray;
color: $gl-text-color;
font-weight: bold;
}
......
......@@ -7,7 +7,7 @@
.timeline-entry {
padding: $gl-padding $gl-btn-padding 11px;
border-color: $white-normal;
color: $gl-gray;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
&:target {
......@@ -32,7 +32,7 @@
.system-note {
.note-text {
color: $gl-gray !important;
color: $gl-text-color !important;
}
}
......
......@@ -98,7 +98,7 @@
&.label-gray {
background-color: $label-gray-bg;
color: $gl-gray;
color: $gl-text-color;
text-shadow: none;
}
......
......@@ -65,11 +65,11 @@ $legend-color: $text-color;
//
//##
$pagination-color: $gl-gray;
$pagination-color: $gl-text-color;
$pagination-bg: $white-light;
$pagination-border: $border-color;
$pagination-hover-color: $gl-gray;
$pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
......@@ -121,6 +121,9 @@ $panel-default-heading-bg: $gray-light;
$panel-footer-bg: $gray-light;
$panel-inner-border: $border-color;
$badge-bg: $badge-bg;
$badge-color: $badge-color;
//== Wells
//
//##
......@@ -154,7 +157,7 @@ $nav-link-padding: 13px $gl-padding;
//
//##
$pre-bg: $gray-light !default;
$pre-color: $gl-gray !default;
$pre-color: $gl-text-color !default;
$pre-border-color: $border-color;
$table-bg-accent: $gray-light;
@mixin md-typography {
color: $md-text-color;
color: $gl-text-color;
word-wrap: break-word;
a {
......@@ -50,14 +50,14 @@
margin: 16px 0 10px;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-gray-dark;
color: $gl-text-color;
}
h2 {
font-size: 1.5em;
font-weight: 600;
margin: 16px 0 10px;
color: $gl-gray-dark;
color: $gl-text-color;
}
h3 {
......@@ -100,7 +100,7 @@
}
p {
color: $gl-text-color-dark;
color: $gl-text-color;
margin: 6px 0 0;
}
......@@ -108,7 +108,7 @@
@extend .table;
@extend .table-bordered;
margin: 12px 0;
color: $gl-text-color-dark;
color: $gl-text-color;
th {
background: $label-gray-bg;
......@@ -230,7 +230,7 @@ h3,
h4,
h5,
h6 {
color: $gl-title-color;
color: $gl-text-color;
font-weight: 600;
}
......@@ -292,7 +292,7 @@ h2,
h3,
h4 {
small {
color: $gl-gray;
color: $gl-text-color;
}
}
......
......@@ -94,29 +94,22 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
$gl-text-color-dark: #5c5d5e;
$gl-text-color-light: #8c8c8c;
$gl-text-color: rgba(0, 0, 0, .85);
$gl-text-color-secondary: rgba(0, 0, 0, .55);
$gl-text-color-disabled: rgba(0, 0, 0, .35);
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3777b0;
$gl-diff-text-color: #555;
$gl-dark-link-color: #333;
$gl-gray-light: #8f8f8f;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
/*
* Lists
*/
$list-font-size: $gl-font-size;
$list-title-color: $gl-title-color;
$list-title-color: $gl-text-color;
$list-text-color: $gl-text-color;
$list-text-disabled-color: #888;
$list-text-disabled-color: $gl-text-color-disabled;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
......@@ -127,7 +120,6 @@ $list-warning-row-color: #8a6d3b;
/*
* Markdown
*/
$md-text-color: $gl-text-color;
$md-link-color: $gl-link-color;
$md-area-border: #ddd;
......@@ -168,9 +160,7 @@ $btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: #cea61b;
$issuable-sidebar-color: #999;
$issuable-avatar-hover-border: #999;
$issuable-clipboard-color: #999;
$issuable-sidebar-color: $gl-text-color-secondary;
$show-aside-bg: #eee;
$show-aside-color: #777;
$show-aside-shadow: #ddd;
......@@ -282,10 +272,8 @@ $btn-active-gray-light: e4e7ed;
/*
* Badges
*/
$badge-bg: #f3f3f3;
$badge-bg-dark: #eee;
$badge-color: #929292;
$badge-color-dark: #8f8f8f;
$badge-bg: #eee;
$badge-color: $gl-text-color-secondary;
/*
* Award emoji
......@@ -304,8 +292,8 @@ $location-icon-color: #e7e9ed;
/*
* Notes
*/
$notes-light-color: #8e8e8e;
$notes-role-color: #8e8e8e;
$notes-light-color: $gl-text-color-secondary;
$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
......@@ -330,7 +318,7 @@ $calendar-user-contrib-text: #959494;
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
$cycle-analytics-big-font: 19px;
$cycle-analytics-dark-text: $gl-title-color;
$cycle-analytics-dark-text: $gl-text-color;
$cycle-analytics-light-gray: #bfbfbf;
$cycle-analytics-dismiss-icon-color: #b2b2b2;
......@@ -382,7 +370,7 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
/*
* Common
*/
$common-gray: $gl-gray;
$common-gray: $gl-text-color;
$common-gray-light: #bbb;
$common-gray-dark: #444;
$common-red: $gl-text-red;
......@@ -537,3 +525,4 @@ Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6;
.info-well {
background: $gray-light;
color: $gl-gray;
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: $border-radius-default;
......
......@@ -40,7 +40,7 @@
}
.zen-control-full {
color: $gl-gray-light;
color: $gl-text-color-secondary;
&:hover {
color: $gl-link-color;
......
......@@ -3,7 +3,7 @@
/*
* White Syntax Colors
*/
$white-code-color: #333;
$white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
......
......@@ -259,7 +259,7 @@
.board-list-count {
padding: 10px 0;
color: $gl-gray-light;
color: $gl-text-color-secondary;
font-size: 13px;
> .fa {
......
......@@ -29,7 +29,7 @@
padding-top: 6px;
padding-bottom: 0;
font-size: 12px;
color: $gl-title-color;
color: $gl-text-color;
display: block;
}
......
......@@ -160,7 +160,7 @@
flex: 1;
a {
color: $gl-gray;
color: $gl-text-color;
&:hover {
color: $gl-link-color;
......@@ -357,7 +357,7 @@
}
.build-light-text {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.build-gutter-toggle {
......
......@@ -18,7 +18,7 @@
}
td {
color: $gl-gray;
color: $gl-text-color;
vertical-align: middle !important;
a {
......
......@@ -19,7 +19,7 @@
.commit-title {
margin: 0;
color: $gl-gray-dark;
color: $gl-text-color;
}
.commit-description {
......@@ -96,14 +96,14 @@
}
.commit-row-message {
color: $gl-dark-link-color;
color: $gl-text-color;
}
}
.text-expander {
display: inline-block;
background: $gray-light;
color: $gl-gray-light;
color: $gl-text-color-secondary;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
......@@ -153,7 +153,7 @@
a,
button {
color: $gl-dark-link-color;
color: $gl-text-color;
vertical-align: baseline;
}
......@@ -176,7 +176,7 @@
}
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
}
......@@ -193,7 +193,7 @@
}
.branch-commit {
color: $gl-gray;
color: $gl-text-color;
.commit-icon {
text-align: center;
......@@ -203,7 +203,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
fill: $gl-gray-light;
fill: $gl-text-color-secondary;
}
}
......@@ -212,6 +212,6 @@
}
.commit-row-message {
color: $gl-gray;
color: $gl-text-color;
}
}
......@@ -111,14 +111,14 @@
line-height: 19px;
font-size: 14px;
font-weight: 600;
color: $gl-title-color;
color: $gl-text-color;
}
&.text {
color: $layout-link-gray;
&.value-col {
color: $gl-title-color;
color: $gl-text-color;
}
}
}
......@@ -260,7 +260,7 @@
.stage-empty,
.not-available {
color: $gl-text-color-light;
color: $gl-text-color-secondary;
}
}
}
......@@ -327,7 +327,7 @@
@include text-overflow();
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
}
}
......@@ -355,7 +355,7 @@
.issue-link,
.commit-author-link,
.issue-author-link {
color: $gl-dark-link-color;
color: $gl-text-color;
}
// Custom CSS for components
......@@ -396,11 +396,11 @@
}
.item-build-name {
color: $gl-title-color;
color: $gl-text-color;
}
.pipeline-id {
color: $gl-title-color;
color: $gl-text-color;
padding: 0 3px 0 0;
}
......@@ -423,7 +423,7 @@
}
.fa {
color: $gl-text-color-light;
color: $gl-text-color-secondary;
font-size: $code_font_size;
}
}
......@@ -435,7 +435,7 @@
width: 75%;
margin: 0 auto;
padding-top: 130px;
color: $gl-text-color-light;
color: $gl-text-color-secondary;
h4 {
color: $gl-text-color;
......
.detail-page-header {
padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
color: $gl-text-color-dark;
color: $gl-text-color;
line-height: 34px;
.author {
color: $gl-text-color-dark;
color: $gl-text-color;
}
.identifier {
color: $gl-text-color-dark;
color: $gl-text-color;
}
.issue_created_ago,
......@@ -22,7 +22,7 @@
.title {
margin: 0 0 16px;
font-size: 2em;
color: $gl-gray-dark;
color: $gl-text-color;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
}
......
......@@ -14,7 +14,7 @@
background: $gray-light;
border-bottom: 1px solid $border-color;
padding: 10px 16px;
color: $gl-diff-text-color;
color: $gl-text-color;
z-index: 10;
border-radius: 3px 3px 0 0;
......@@ -50,7 +50,7 @@
overflow: auto;
overflow-y: hidden;
background: $white-light;
color: $gl-title-color;
color: $gl-text-color;
border-radius: 0 0 3px 3px;
.unfold {
......@@ -380,7 +380,7 @@
}
cursor: default;
color: $gl-title-color;
color: $gl-text-color;
}
&.disabled {
......
......@@ -72,25 +72,25 @@
.external-url,
.dropdown-new {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.dropdown-menu {
.fa {
margin-right: 6px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
.build-link,
.branch-name {
color: $gl-dark-link-color;
color: $gl-text-color;
}
.stop-env-link,
.external-url {
color: $gl-gray-light;
color: $gl-text-color-secondary;
.stop-env-icon {
font-size: 14px;
......@@ -101,7 +101,7 @@
.build-column {
.build-link {
color: $gl-dark-link-color;
color: $gl-text-color;
}
.avatar {
......
......@@ -21,7 +21,7 @@
}
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
.avatar {
......
......@@ -13,7 +13,7 @@
.stats {
float: right;
line-height: $list-text-height;
color: $gl-gray;
color: $gl-text-color;
span {
margin-right: 15px;
......
......@@ -103,7 +103,7 @@
}
.edit-link {
color: $gl-gray;
color: $gl-text-color;
&:hover {
color: $md-link-color;
......@@ -139,7 +139,7 @@
}
.btn-clipboard:hover {
color: $gl-gray;
color: $gl-text-color;
}
}
......@@ -174,7 +174,7 @@
}
.no-value {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.sidebar-collapsed-icon {
......@@ -242,7 +242,7 @@
color: $issuable-sidebar-color;
&:hover {
color: $gl-gray;
color: $gl-text-color;
}
span {
......@@ -255,16 +255,16 @@
}
.avatar:hover {
border-color: $issuable-avatar-hover-border;
border-color: $issuable-sidebar-color;
}
.btn-clipboard {
border: none;
color: $issuable-clipboard-color;
color: $issuable-sidebar-color;
&:hover {
background: transparent;
color: $gl-gray;
color: $gl-text-color;
}
}
}
......@@ -338,7 +338,7 @@
margin-left: 5px;
a {
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
......
......@@ -117,7 +117,7 @@
.manage-labels-list {
.btn-action {
color: $gl-dark-link-color;
color: $gl-text-color;
.fa {
font-size: 18px;
......
......@@ -17,14 +17,19 @@
line-height: 1.5;
p {
font-size: 18px;
font-size: 16px;
color: $login-brand-holder-color;
}
h1:first-child {
font-weight: normal;
margin-bottom: 30px;
margin-bottom: 0.68em;
margin-top: 0;
font-size: 34px;
}
h3 {
font-size: 22px;
}
img {
......
......@@ -4,7 +4,7 @@
*/
.mr-state-widget {
background: $gray-light;
color: $gl-gray;
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 2px;
......@@ -58,7 +58,7 @@
padding-right: 0;
a {
color: $gl-gray;
color: $gl-text-color;
}
}
......@@ -70,7 +70,7 @@
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-gray;
color: $gl-text-color;
svg {
margin-right: 4px;
......@@ -94,7 +94,7 @@
}
.normal {
color: $gl-text-color-dark;
color: $gl-text-color;
}
.js-deployment-link {
......@@ -106,7 +106,7 @@
font-weight: 600;
font-size: 16px;
margin: 5px 0;
color: $gl-gray-dark;
color: $gl-text-color;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
......@@ -190,7 +190,7 @@
}
.label-branch {
color: $gl-gray-dark;
color: $gl-text-color;
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
......@@ -363,7 +363,7 @@
th {
background-color: $white-light;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
}
}
......
......@@ -102,7 +102,7 @@
margin-top: 7px;
.issuable-number {
color: $gl-gray-light;
color: $gl-text-color-secondary;
margin-right: 5px;
}
......
......@@ -27,6 +27,7 @@
.new-note,
.note-edit-form {
.note-form-actions {
position: relative;
margin-top: $gl-padding;
}
......@@ -44,7 +45,7 @@
.note-textarea {
display: block;
padding: 10px 0;
color: $gl-gray;
color: $gl-text-color;
font-family: $regular_font;
border: 0;
......@@ -204,7 +205,7 @@
.comment-toolbar {
padding-top: $gl-padding-top;
color: $gl-gray-light;
color: $gl-text-color-secondary;
border-top: 1px solid $border-color;
}
......@@ -265,3 +266,18 @@
}
}
}
.note-edit-warning.settings-message {
display: none;
padding: 5px 10px;
position: absolute;
left: 127px;
top: 2px;
@media (max-width: $screen-xs-max) {
position: relative;
top: 0;
left: 0;
margin-bottom: 10px;
}
}
......@@ -345,7 +345,7 @@ ul.notes {
}
.author_link {
color: $gl-gray;
color: $gl-text-color;
}
}
......@@ -588,13 +588,11 @@ ul.notes {
// Merge request notes in diffs
.diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-headline-light {
display: block;
position: relative;
}
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
......
......@@ -140,7 +140,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
fill: $gl-gray-light;
fill: $gl-text-color-secondary;
}
.fa {
......@@ -186,12 +186,13 @@
.stage-cell {
font-size: 0;
svg {
height: 18px;
width: 18px;
position: relative;
> .stage-container > button > svg {
height: 22px;
width: 22px;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
vertical-align: middle;
overflow: visible;
}
......@@ -209,7 +210,7 @@
content: '';
width: 8px;
position: absolute;
right: -7px;
right: -8px;
top: 10px;
border-bottom: 2px solid $border-color;
}
......@@ -219,7 +220,7 @@
.duration,
.finished-at {
color: $gl-gray-light;
color: $gl-text-color-secondary;
margin: 4px 0;
.fa {
......@@ -240,7 +241,7 @@
.btn {
margin: 0;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.cancel-retry-btns {
......@@ -253,10 +254,10 @@
.dropdown-toggle,
.dropdown-menu {
color: $gl-gray-light;
color: $gl-text-color-secondary;
.fa {
color: $gl-gray-light;
color: $gl-text-color-secondary;
font-size: 14px;
}
......@@ -290,7 +291,7 @@
.build-link {
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
}
......@@ -347,13 +348,14 @@
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
ul {
.stage-column-list,
.builds-container > ul {
padding: 0;
}
a {
text-decoration: none;
color: $gl-text-color-light;
color: $gl-text-color-secondary;
}
svg {
......@@ -477,13 +479,54 @@
width: 186px;
margin-bottom: 10px;
white-space: normal;
color: $gl-text-color-light;
color: $gl-text-color-secondary;
// Action Icons in big pipeline-graph nodes
> .ci-action-icon-container .ci-action-icon-wrapper {
i {
color: $border-color;
border-radius: 100%;
border: 1px solid $border-color;
padding: 5px 6px;
font-size: 13px;
background: $white-light;
height: 30px;
width: 30px;
&::before {
position: relative;
top: 3px;
left: 3px;
}
&:hover {
color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-bg;
}
}
.ci-play-icon {
padding: 5px 5px 5px 7px;
}
}
> .ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
}
.ci-status-icon svg {
height: 20px;
width: 20px;
}
.dropdown-menu-toggle {
background-color: transparent;
border: none;
padding: 0;
color: $gl-text-color-light;
color: $gl-text-color-secondary;
&:focus {
outline: none;
......@@ -513,16 +556,6 @@
}
}
> .ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
}
.ci-status-icon svg {
height: 20px;
width: 20px;
}
.arrow {
&::before,
......@@ -605,29 +638,9 @@
}
}
}
.grouped-pipeline-dropdown {
.dropdown-build {
.build-content {
width: 100%;
&:hover {
background-color: $stage-hover-bg;
color: $gl-text-color;
}
}
.ci-action-icon-container {
padding: 0;
font-size: 11px;
position: absolute;
top: 1px;
right: 8px;
}
}
}
}
// Triggers the dropdown in the big pipeline graph
.dropdown-counter-badge {
color: $border-color;
font-weight: 100;
......@@ -637,66 +650,6 @@
top: 8px;
}
.grouped-pipeline-dropdown {
padding: 0;
width: 195px;
min-width: 195px;
left: auto;
right: -195px;
top: -4px;
box-shadow: 0 1px 5px $black-transparent;
a {
display: inline-block;
}
.dropdown-build {
.build-content {
width: 100%;
&:hover {
background-color: $stage-hover-bg;
color: $gl-text-color;
}
}
.ci-action-icon-container {
padding: 0;
font-size: 11px;
position: absolute;
margin-top: 3px;
right: 7px;
}
}
ul {
max-height: 245px;
overflow: auto;
margin: 3px 0;
li {
margin: 4px 8px 4px 9px;
padding: 0;
line-height: 1.1;
position: relative;
.ci-action-icon-container:hover {
background-color: transparent;
}
.ci-status-icon {
position: relative;
top: 2px;
}
}
}
}
.pipeline-graph .dropdown-build .ci-status-icon svg {
width: 18px;
height: 18px;
}
.ci-status-text {
max-width: 110px;
white-space: nowrap;
......@@ -708,177 +661,233 @@
font-weight: 200;
}
// Action Icons
.ci-action-icon-container .ci-action-icon-wrapper {
i {
color: $border-color;
border-radius: 100%;
border: 1px solid $border-color;
padding: 5px 6px;
font-size: 13px;
background: $white-light;
height: 30px;
width: 30px;
&::before {
// Dropdown button in mini pipeline graph
.mini-pipeline-graph-dropdown-toggle {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
border-style: solid;
width: 22px;
height: 22px;
margin: 0;
padding: 0;
transition: all 0.2s linear;
position: relative;
top: 3px;
left: 3px;
> .fa.fa-caret-down {
position: absolute;
left: 20px;
top: 5px;
display: inline-block;
visibility: hidden;
opacity: 0;
color: inherit;
font-size: 12px;
transition: visibility 0.1s, opacity 0.1s linear;
}
&:active,
&:focus,
&:hover {
color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-bg;
}
}
outline: none;
width: 35px;
.ci-play-icon {
padding: 5px 5px 5px 7px;
.fa.fa-caret-down {
visibility: visible;
opacity: 1;
}
}
.dropdown-build {
color: $gl-text-color-light;
.build-content {
padding: 4px 7px 8px;
}
.ci-action-icon-container {
padding: 0;
font-size: 11px;
float: right;
margin-top: 3px;
display: inline-block;
position: relative;
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
border-color: $gl-success;
color: $gl-success;
i {
font-size: 11px;
margin-top: 0;
&:hover,
&:focus,
&:active {
background-color: rgba($gl-success, 0.1);
border-color: $gl-success;
}
}
.ci-action-icon-container {
i {
width: 24px;
height: 24px;
&.ci-status-icon-failed {
border-color: $gl-danger;
color: $gl-danger;
&::before {
top: 1px;
left: 1px;
}
&:hover,
&:focus,
&:active {
background-color: rgba($gl-danger, 0.1);
border-color: $gl-danger;
}
}
.stage {
max-width: 100px;
width: 100px;
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
border-color: $gl-warning;
color: $gl-warning;
.ci-status-icon svg {
height: 18px;
width: 18px;
&:hover,
&:focus,
&:active {
background-color: rgba($gl-warning, 0.1);
border-color: $gl-warning;
}
.ci-status-text {
max-width: 95px;
}
}
/**
* Builds dropdown in mini pipeline
*/
.mini-pipeline-graph {
.builds-dropdown {
background-color: transparent;
padding: 0;
color: $gl-text-color-light;
border: none;
margin: 0;
&.ci-status-icon-running {
border-color: $blue-normal;
color: $blue-normal;
&:hover,
&:focus,
&:hover {
outline: none;
margin-right: -8px;
&:active {
background-color: rgba($blue-normal, 0.1);
border-color: $blue-normal;
}
}
.ci-status-icon {
width: 32px;
padding: 0 8px 0 0;
transition: width 0.1s cubic-bezier(0.25, 0, 1, 1);
&.ci-status-icon-canceled,
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
border-color: $gl-text-color;
color: $gl-text-color;
+ .dropdown-caret {
visibility: visible;
opacity: 1;
}
&:hover,
&:focus,
&:active {
background-color: rgba($gl-text-color, 0.1);
border-color: $gl-text-color;
}
}
&.ci-status-icon-created,
&.ci-status-icon-skipped {
border-color: $gray-darkest;
color: $gray-darkest;
&:hover,
&:focus,
&:active {
.ci-status-icon-success {
background-color: rgba($gl-success, .1);
background-color: rgba($gray-darkest, 0.1);
border-color: $gray-darkest;
}
.ci-status-icon-failed {
background-color: rgba($gl-danger, .1);
}
}
// dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
width: 195px;
max-width: 195px;
.ci-status-icon-pending,
.ci-status-icon-success_with_warnings {
background-color: rgba($gl-warning, .1);
li {
padding: 2px 3px;
}
.ci-status-icon-running {
background-color: rgba($blue-normal, .1);
.scrollable-menu {
max-height: 245px;
overflow: auto;
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
background-color: rgba($gl-gray, .1);
// Loading icon
.builds-dropdown-loading {
margin: 0 auto;
width: 20px;
}
.ci-status-icon-created,
.ci-status-icon-skipped {
background-color: rgba($gray-darkest, .1);
// Action icon on the right
a.ci-action-icon-wrapper {
color: $action-icon-color;
border: 1px solid $action-icon-color;
border-radius: 20px;
width: 22px;
height: 22px;
padding: 2px 0 0 5px;
cursor: pointer;
float: right;
margin: -26px 9px 0 0;
font-size: 12px;
background-color: $white-light;
&:hover,
&:focus {
text-decoration: none;
color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid transparent;
}
}
.mini-pipeline-graph-icon-container {
.dropdown-caret {
font-size: 11px;
position: absolute;
top: 6px;
left: 20px;
margin-right: -6px;
z-index: 2;
visibility: hidden;
opacity: 0;
transition: visibility 0.1s, opacity 0.1s linear;
// link to the build
.mini-pipeline-graph-dropdown-item {
padding: 3px 7px 4px;
clear: both;
font-weight: normal;
line-height: 1.428571429;
white-space: nowrap;
margin: 0 5px;
border-radius: 3px;
// build name
.ci-build-text {
font-weight: 200;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 90px;
color: $gl-text-color-secondary;
margin-left: 2px;
display: inline-block;
top: 1px;
vertical-align: text-bottom;
position: relative;
}
// status icon on the left
.ci-status-icon {
top: 3px;
position: relative;
> svg {
overflow: visible;
width: 18px;
height: 18px;
}
}
.dropdown-build .build-content {
padding: 3px 7px 7px;
&:hover,
&:focus {
outline: none;
text-decoration: none;
color: $gl-text-color;
background-color: $stage-hover-bg;
}
.builds-dropdown-loading {
margin: 10px auto;
width: 18px;
}
}
.grouped-pipeline-dropdown {
right: -172px;
top: 23px;
min-height: 50px;
// Dropdown in the big pipeline graph
.big-pipeline-graph-dropdown-menu {
width: 195px;
min-width: 195px;
left: auto;
right: -195px;
top: -4px;
box-shadow: 0 1px 5px $black-transparent;
a {
color: $gl-text-color-light;
.mini-pipeline-graph-dropdown-item {
.ci-status-icon {
top: -1px;
}
}
}
/**
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu {
.arrow-up {
&::before,
&::after {
......@@ -907,31 +916,8 @@
}
/**
* Icons in mini pipeline graph
* Terminal
*/
.mini-pipeline-graph-icon-container .ci-status-icon {
display: inline-block;
border: 1px solid;
border-radius: 22px;
margin-right: 1px;
width: 22px;
height: 22px;
position: relative;
z-index: 2;
transition: all 0.1s cubic-bezier(0.25, 0, 1, 1);
svg {
top: -1px;
left: -1px;
}
}
.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg {
width: 22px;
height: 22px;
}
.terminal-icon {
margin-left: 3px;
}
......
......@@ -292,7 +292,7 @@
.option-title {
font-weight: normal;
display: inline-block;
color: $gl-gray-dark;
color: $gl-text-color;
}
.option-descr {
......@@ -331,7 +331,7 @@
a.deploy-project-label {
padding: 5px;
margin-right: 5px;
color: $gl-gray;
color: $gl-text-color;
background-color: $row-hover;
&:hover {
......@@ -372,7 +372,7 @@ a.deploy-project-label {
}
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
.dropdown-menu {
......@@ -426,7 +426,7 @@ a.deploy-project-label {
width: 100%;
height: 100%;
padding-top: $gl-padding;
color: $gl-gray;
color: $gl-text-color;
.caption {
min-height: 30px;
......@@ -552,7 +552,7 @@ pre.light-well {
margin: 0 7px 7px;
h5 {
color: $gl-text-color-dark;
color: $gl-text-color;
}
.light-well {
......@@ -662,7 +662,7 @@ pre.light-well {
}
.commit-row-message {
color: $gl-gray;
color: $gl-text-color;
}
.commit_short_id {
......@@ -750,7 +750,7 @@ pre.light-well {
.protected-branches-list {
a {
color: $gl-gray;
color: $gl-text-color;
&:hover {
color: $gl-link-color;
......
.settings-list-icon {
color: $gl-gray-light;
color: $gl-text-color-secondary;
font-size: $settings-icon-size;
line-height: 42px;
}
......
......@@ -61,15 +61,15 @@
&.ci-canceled,
&.ci-disabled {
color: $gl-gray;
border-color: $gl-gray;
color: $gl-text-color;
border-color: $gl-text-color;
&:not(span):hover {
background-color: rgba($gl-gray, .07);
background-color: rgba($gl-text-color, .07);
}
svg {
fill: $gl-gray;
fill: $gl-text-color;
}
}
......@@ -101,15 +101,15 @@
&.ci-created,
&.ci-skipped {
color: $gl-gray-light;
border-color: $gl-gray-light;
color: $gl-text-color-secondary;
border-color: $gl-text-color-secondary;
&:not(span):hover {
background-color: rgba($gl-gray-light, .07);
background-color: rgba($gl-text-color-secondary, .07);
}
svg {
fill: $gl-gray-light;
fill: $gl-text-color-secondary;
}
}
......
......@@ -90,7 +90,7 @@
}
p {
color: $gl-text-color-dark;
color: $gl-text-color;
}
}
......
......@@ -78,7 +78,7 @@
i,
a {
color: $gl-dark-link-color;
color: $gl-text-color;
}
img {
......@@ -104,21 +104,21 @@
padding-right: 8px;
.commit-author-name {
color: $gl-gray;
color: $gl-text-color;
}
}
.tree-time-ago {
min-width: 135px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.tree-commit {
max-width: 320px;
color: $gl-gray-light;
color: $gl-text-color-secondary;
.tree-commit-link {
color: $gl-gray-light;
color: $gl-text-color-secondary;
&:hover {
text-decoration: underline;
......
......@@ -15,7 +15,7 @@
}
.wiki-last-edit-by {
color: $gl-gray-light;
color: $gl-text-color-secondary;
strong {
color: $gl-text-color;
......@@ -24,7 +24,7 @@
.light {
font-weight: normal;
color: $gl-gray-light;
color: $gl-text-color-secondary;
}
.git-access-header {
......
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
include EncodingHelper
include Gitlab::Git::EncodingHelper
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
......
-# Renders the content of each li in the dropdown
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status.group}"
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do
%span{ class: klass }= custom_icon(status.icon)
%span.ci-build-text= subject.name
- else
.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } }
%span{ class: klass }= custom_icon(status.icon)
%span.ci-build-text= subject.name
- if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
= icon(status.action_icon, class: status.action_class)
......@@ -31,7 +31,7 @@
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
Abuse Reports
%span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all))
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- if askimet_enabled?
= nav_link(controller: :spam_logs) do
......
......@@ -26,13 +26,13 @@
%span
Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.badge-dark.count= number_with_delimiter(issues.count)
%span.badge.count= number_with_delimiter(issues.count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.badge-dark.count= number_with_delimiter(merge_requests.count)
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
%span
......
......@@ -61,14 +61,14 @@
%span
Issues
- if @project.default_issues_tracker?
%span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
%span.badge.badge-dark.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
......
......@@ -47,22 +47,19 @@
- icon_status = "#{detailed_status.icon}_borderless"
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.mini-pipeline-graph
.dropdown.inline.build-content
%button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
%span.has-tooltip{ class: status_klass }
%span.mini-pipeline-graph-icon-container
%span{ class: status_klass }= custom_icon(icon_status)
= icon('caret-down', class: 'dropdown-caret')
.stage-container.dropdown.js-mini-pipeline-graph
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
= custom_icon(icon_status)
= icon('caret-down')
.js-builds-dropdown-container
.dropdown-menu.grouped-pipeline-dropdown
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
.arrow-up
.js-builds-dropdown-list
.js-builds-dropdown-list.scrollable-menu
.js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
%td
- if pipeline.duration
%p.duration
......
%ul.content-list.pipelines
%div
- if pipelines.blank?
%li
%div
.nothing-here-block No pipelines to show
- else
.table-holder
.table-holder.pipelines
%table.table.ci-table.js-pipeline-table
%thead
%th.pipeline-status Status
......
.note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= form_tag '#', method: :put, remote: true, class: 'edit-note common-note-form js-quick-submit' do
= hidden_field_tag :authenticity_token, form_authenticity_token
= hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do
= render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
.settings-message.note-edit-warning.js-edit-warning
Finish editing this message first!
= submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
......@@ -67,7 +67,9 @@
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list #{note.note}
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
......
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
= render 'projects/notes/edit_form'
%ul.notes.notes-form.timeline
%li.timeline-entry
.flash-container.timeline-content
......
%ul
- @stage.statuses.latest.each do |status|
%li.dropdown-build
= render 'ci/status/graph_badge', subject: status
- @stage.statuses.latest.each do |status|
%li
= render 'ci/status/dropdown_graph_badge', subject: status
......@@ -5,9 +5,10 @@
%span.ci-status-text
= name
%span.dropdown-counter-badge= subject.size
.dropdown-menu.grouped-pipeline-dropdown
%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown
.arrow
%ul
.scrollable-menu
- subject.each do |status|
%li.dropdown-build
= render 'ci/status/graph_badge', subject: status
%li
= render 'ci/status/dropdown_graph_badge', subject: status
---
title: Allow group and project paths when transferring projects via the API
merge_request:
author:
---
title: 25701 standardize text colors
merge_request:
author:
---
title: Removes unneeded `window` declaration in environments related code
merge_request: 8456
author:
---
title: Fix Commits API to accept a Project path upon POST
merge_request:
author:
---
title: Removes invalid html and unneed CSS to prevent shaking in the pipelines tab
merge_request: 8411
author:
---
title: Decreases font-size on login page
merge_request:
author:
---
title: Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown
merge_request: 8443
author:
---
title: Refactored note edit form to improve frontend performance on MR and Issues
pages, especially pages with has a lot of discussions in it
merge_request: 8356
author:
......@@ -2,6 +2,12 @@ Rails.application.configure do |config|
config.middleware.use(Gitlab::Middleware::Multipart)
end
# The Gitlab::Middleware::Multipart middleware inserts instances of our
# own ::UploadedFile class in the Rack env of requests. These instances
# will be blocked by the 'strong parameters' feature of ActionController
# unless we somehow whitelist them. At the moment it seems the only way
# to do that is by monkey-patching.
#
module Gitlab
module StrongParameterScalars
GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile]
......
......@@ -335,7 +335,7 @@ POST /groups/:id/projects/:project_id
Parameters:
- `id` (required) - The ID or path of a group
- `project_id` (required) - The ID of a project
- `project_id` (required) - The ID or path of a project
## Update group
......
......@@ -58,6 +58,13 @@ GitLab uses Font Awesome icons throughout our interface.
| ![Red](img/color-red.png) | Closed | Delete and other destructive commands |
| ![Grey](img/color-grey.png) | Neutral | Neutral secondary commands |
### Text colors
|||
| :---: | :--- |
| ![Text primary](img/color-textprimary.png) | Used for primary body text, such as issue description and comment |
| ![Text secondary](img/color-textsecondary.png) | Used for secondary body text, such as username and date |
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
---
......
......@@ -12,6 +12,7 @@
* [Panels](#panels)
* [Alerts](#alerts)
* [Forms](#forms)
* [Search box](#search-box)
* [File holders](#file-holders)
* [Data formats](#data-formats)
......@@ -215,6 +216,18 @@ Horizontal form (`form.horizontal-form`) with label rendered inline with input.
---
## Search box
Search boxes across GitLab have a consistent rest, active and text entered state. When text isn't entered in the box, there should be a magnifying glass right aligned with the input field. When text is entered, the magnifying glass should become a x, allowing users to clear their text.
![Search box](img/components-searchbox.png)
If needed, we indicate the scope of the search in the search box.
![Scoped Search box](img/components-searchboxscoped.png)
---
## File holders
A file holder (`.file-holder`) is used to show the contents of a file inline on a page of GitLab.
......
@admin
Feature: Admin Deploy Keys
Background:
Given I sign in as an admin
And there are public deploy keys in system
Scenario: Deploy Keys list
When I visit admin deploy keys page
Then I should see all public deploy keys
Scenario: Deploy Keys new
When I visit admin deploy keys page
And I click 'New Deploy Key'
And I submit new deploy key
Then I should be on admin deploy keys page
And I should see newly created deploy key without write access
Scenario: Deploy Keys new with write access
When I visit admin deploy keys page
And I click 'New Deploy Key'
And I submit new deploy key with write access
Then I should be on admin deploy keys page
And I should see newly created deploy key with write access
class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedAdmin
step 'there are public deploy keys in system' do
create(:deploy_key, public: true)
create(:another_deploy_key, public: true)
end
step 'I should see all public deploy keys' do
DeployKey.are_public.each do |p|
expect(page).to have_content p.title
end
end
step 'I visit admin deploy key page' do
visit admin_deploy_key_path(deploy_key)
end
step 'I visit admin deploy keys page' do
visit admin_deploy_keys_path
end
step 'I click \'New Deploy Key\'' do
click_link 'New Deploy Key'
end
step 'I submit new deploy key' do
fill_in "deploy_key_title", with: "laptop"
fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
click_button "Create"
end
step 'I submit new deploy key with write access' do
fill_in "deploy_key_title", with: "server"
fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
check "deploy_key_can_push"
click_button "Create"
end
step 'I should be on admin deploy keys page' do
expect(current_path).to eq admin_deploy_keys_path
end
step 'I should see newly created deploy key without write access' do
expect(page).to have_content(deploy_key.title)
expect(page).to have_content('No')
end
step 'I should see newly created deploy key with write access' do
expect(page).to have_content(deploy_key.title)
expect(page).to have_content('Yes')
end
def deploy_key
@deploy_key ||= DeployKey.are_public.first
end
end
......@@ -44,7 +44,6 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
requires :id, type: Integer, desc: 'The project ID'
requires :branch_name, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
......
......@@ -156,12 +156,12 @@ module API
success Entities::GroupDetail
end
params do
requires :project_id, type: String, desc: 'The ID of the project'
requires :project_id, type: String, desc: 'The ID or path of the project'
end
post ":id/projects/:project_id" do
authenticated_as_admin!
group = Group.find_by(id: params[:id])
project = Project.find(params[:project_id])
group = find_group!(params[:id])
project = find_project!(params[:project_id])
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
......
......@@ -543,6 +543,13 @@ module API
type: String,
desc: 'The Mattermost token'
}
],
'slack-slash-commands' => [
{
name: :token,
type: String,
desc: 'The Slack token'
}
]
}.freeze
......
module Gitlab
module Git
# Class for parsing Git attribute files and extracting the attributes for
# file patterns.
#
# Unlike Rugged this parser only needs a single IO call (a call to `open`),
# vastly reducing the time spent in extracting attributes.
#
# This class _only_ supports parsing the attributes file located at
# `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
# (`.gitattributes` is copied to this particular path).
#
# Basic usage:
#
# attributes = Gitlab::Git::Attributes.new(some_repo.path)
#
# attributes.attributes('README.md') # => { "eol" => "lf }
class Attributes
# path - The path to the Git repository.
def initialize(path)
@path = File.expand_path(path)
@patterns = nil
end
# Returns all the Git attributes for the given path.
#
# path - A path to a file for which to get the attributes.
#
# Returns a Hash.
def attributes(path)
full_path = File.join(@path, path)
patterns.each do |pattern, attrs|
return attrs if File.fnmatch?(pattern, full_path)
end
{}
end
# Returns a Hash containing the file patterns and their attributes.
def patterns
@patterns ||= parse_file
end
# Parses an attribute string.
#
# These strings can be in the following formats:
#
# text # => { "text" => true }
# -text # => { "text" => false }
# key=value # => { "key" => "value" }
#
# string - The string to parse.
#
# Returns a Hash containing the attributes and their values.
def parse_attributes(string)
values = {}
dash = '-'
equal = '='
binary = 'binary'
string.split(/\s+/).each do |chunk|
# Data such as "foo = bar" should be treated as "foo" and "bar" being
# separate boolean attributes.
next if chunk == equal
key = chunk
# Input: "-foo"
if chunk.start_with?(dash)
key = chunk.byteslice(1, chunk.length - 1)
value = false
# Input: "foo=bar"
elsif chunk.include?(equal)
key, value = chunk.split(equal, 2)
# Input: "foo"
else
value = true
end
values[key] = value
# When the "binary" option is set the "diff" option should be set to
# the inverse. If "diff" is later set it should overwrite the
# automatically set value.
values['diff'] = false if key == binary && value
end
values
end
# Iterates over every line in the attributes file.
def each_line
full_path = File.join(@path, 'info/attributes')
return unless File.exist?(full_path)
File.open(full_path, 'r') do |handle|
handle.each_line do |line|
break unless line.valid_encoding?
yield line.strip
end
end
end
private
# Parses the Git attributes file.
def parse_file
pairs = []
comment = '#'
each_line do |line|
next if line.start_with?(comment) || line.empty?
pattern, attrs = line.split(/\s+/, 2)
parsed = attrs ? parse_attributes(attrs) : {}
pairs << [File.join(@path, pattern), parsed]
end
# Newer entries take precedence over older entries.
pairs.reverse.to_h
end
end
end
end
require_relative 'encoding_helper'
module Gitlab
module Git
class Blame
include Gitlab::Git::EncodingHelper
attr_reader :lines, :blames
def initialize(repository, sha, path)
@repo = repository
@sha = sha
@path = path
@lines = []
@blames = load_blame
end
def each
@blames.each do |blame|
yield(
Gitlab::Git::Commit.new(blame.commit),
blame.line
)
end
end
private
def load_blame
cmd = %W(git --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
# Read in binary mode to ensure ASCII-8BIT
raw_output = IO.popen(cmd, 'rb') {|io| io.read }
output = encode_utf8(raw_output)
process_raw_blame output
end
def process_raw_blame(output)
lines, final = [], []
info, commits = {}, {}
# process the output
output.split("\n").each do |line|
if line[0, 1] == "\t"
lines << line[1, line.size]
elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
commits[commit_id] = nil unless commits.key?(commit_id)
info[lineno] = [commit_id, old_lineno]
end
end
# load all commits in single call
commits.keys.each do |key|
commits[key] = @repo.lookup(key)
end
# get it together
info.sort.each do |lineno, (commit_id, old_lineno)|
commit = commits[commit_id]
final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1])
end
@lines = final
end
end
class BlameLine
attr_accessor :lineno, :oldlineno, :commit, :line
def initialize(lineno, oldlineno, commit, line)
@lineno = lineno
@oldlineno = oldlineno
@commit = commit
@line = line
end
end
end
end
require_relative 'encoding_helper'
require_relative 'path_helper'
module Gitlab
module Git
class Blob
include Linguist::BlobHelper
include Gitlab::Git::EncodingHelper
# This number is the maximum amount of data that we want to display to
# the user. We load as much as we can for encoding detection
# (Linguist) and LFS pointer parsing. All other cases where we need full
# blob data should use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10485760
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
class << self
def find(repository, sha, path)
commit = repository.lookup(sha)
root_tree = commit.tree
blob_entry = find_entry_by_path(repository, root_tree.oid, path)
return nil unless blob_entry
if blob_entry[:type] == :commit
submodule_blob(blob_entry, path, sha)
else
blob = repository.lookup(blob_entry[:oid])
if blob
new(
id: blob.oid,
name: blob_entry[:name],
size: blob.size,
data: blob.content(MAX_DATA_DISPLAY_SIZE),
mode: blob_entry[:filemode].to_s(8),
path: path,
commit_id: sha,
binary: blob.binary?
)
end
end
end
def raw(repository, sha)
blob = repository.lookup(sha)
new(
id: blob.oid,
size: blob.size,
data: blob.content(MAX_DATA_DISPLAY_SIZE),
binary: blob.binary?
)
end
# Recursive search of blob id by path
#
# Ex.
# blog/ # oid: 1a
# app/ # oid: 2a
# models/ # oid: 3a
# file.rb # oid: 4a
#
#
# Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
#
def find_entry_by_path(repository, root_id, path)
root_tree = repository.lookup(root_id)
# Strip leading slashes
path[/^\/*/] = ''
path_arr = path.split('/')
entry = root_tree.find do |entry|
entry[:name] == path_arr[0]
end
return nil unless entry
if path_arr.size > 1
return nil unless entry[:type] == :tree
path_arr.shift
find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
else
[:blob, :commit].include?(entry[:type]) ? entry : nil
end
end
def submodule_blob(blob_entry, path, sha)
new(
id: blob_entry[:oid],
name: blob_entry[:name],
data: '',
path: path,
commit_id: sha,
)
end
# Commit file in repository and return commit sha
#
# options should contain next structure:
# file: {
# content: 'Lorem ipsum...',
# path: 'documents/story.txt',
# update: true
# },
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Wow such commit',
# branch: 'master',
# update_ref: false
# }
#
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def commit(repository, options, action = :add)
file = options[:file]
update = file[:update].nil? ? true : file[:update]
author = options[:author]
committer = options[:committer]
commit = options[:commit]
repo = repository.rugged
ref = commit[:branch]
update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
parents = []
mode = 0o100644
unless ref.start_with?('refs/')
ref = 'refs/heads/' + ref
end
path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
# Abort if any invalid characters remain (e.g. ../foo)
raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
filename = path_name.to_s
index = repo.index
unless repo.empty?
rugged_ref = repo.references[ref]
raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
last_commit = rugged_ref.target
index.read_tree(last_commit.tree)
parents = [last_commit]
end
if action == :remove
index.remove(filename)
else
file_entry = index.get(filename)
if action == :rename
old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
old_filename = old_path_name.to_s
file_entry = index.get(old_filename)
index.remove(old_filename) unless file_entry.blank?
end
if file_entry
raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
# Preserve the current file mode if one is available
mode = file_entry[:mode] if file_entry[:mode]
end
content = file[:content]
detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
unless detect && detect[:type] == :binary
# When writing to the repo directly as we are doing here,
# the `core.autocrlf` config isn't taken into account.
content.gsub!("\r\n", "\n") if repository.autocrlf
end
oid = repo.write(content, :blob)
index.add(path: filename, oid: oid, mode: mode)
end
opts = {}
opts[:tree] = index.write_tree(repo)
opts[:author] = author
opts[:committer] = committer
opts[:message] = commit[:message]
opts[:parents] = parents
opts[:update_ref] = ref if update_ref
Rugged::Commit.create(repo, opts)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# Remove file from repository and return commit sha
#
# options should contain next structure:
# file: {
# path: 'documents/story.txt'
# },
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Remove FILENAME',
# branch: 'master'
# }
#
def remove(repository, options)
commit(repository, options, :remove)
end
# Rename file from repository and return commit sha
#
# options should contain next structure:
# file: {
# previous_path: 'documents/old_story.txt'
# path: 'documents/story.txt'
# content: 'Lorem ipsum...',
# update: true
# },
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Rename FILENAME',
# branch: 'master'
# }
#
def rename(repository, options)
commit(repository, options, :rename)
end
end
def initialize(options)
%w(id name path size data mode commit_id binary).each do |key|
self.send("#{key}=", options[key.to_sym])
end
@loaded_all_data = false
# Retain the actual size before it is encoded
@loaded_size = @data.bytesize if @data
end
def binary?
@binary.nil? ? super : @binary == true
end
def empty?
!data || data == ''
end
def data
encode! @data
end
# Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into
# memory as a Ruby string.
def load_all_data!(repository)
return if @data == '' # don't mess with submodule blobs
return @data if @loaded_all_data
@loaded_all_data = true
@data = repository.lookup(id).content
@loaded_size = @data.bytesize
end
def name
encode! @name
end
# Valid LFS object pointer is a text file consisting of
# version
# oid
# size
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
def lfs_pointer?
has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
end
def lfs_oid
if has_lfs_version_key?
oid = data.match(/(?<=sha256:)([0-9a-f]{64})/)
return oid[1] if oid
end
nil
end
def lfs_size
if has_lfs_version_key?
size = data.match(/(?<=size )([0-9]+)/)
return size[1] if size
end
nil
end
def truncated?
size && (size > loaded_size)
end
private
def has_lfs_version_key?
!empty? && text? && data.start_with?("version https://git-lfs.github.com/spec")
end
end
end
end
module Gitlab
module Git
class BlobSnippet
include Linguist::BlobHelper
attr_accessor :ref
attr_accessor :lines
attr_accessor :filename
attr_accessor :startline
def initialize(ref, lines, startline, filename)
@ref, @lines, @startline, @filename = ref, lines, startline, filename
end
def data
lines.join("\n") if lines
end
def name
filename
end
def size
data.length
end
def mode
nil
end
end
end
end
module Gitlab
module Git
class Branch < Ref
end
end
end
# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
module Gitlab
module Git
class Commit
include Gitlab::Git::EncodingHelper
attr_accessor :raw_commit, :head, :refs
SERIALIZE_KEYS = [
:id, :message, :parent_ids,
:authored_date, :author_name, :author_email,
:committed_date, :committer_name, :committer_email
].freeze
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
methods = [:message, :parent_ids, :authored_date, :author_name,
:author_email, :committed_date, :committer_name,
:committer_email]
methods.all? do |method|
send(method) == other.send(method)
end
end
class << self
# Get commits collection
#
# Ex.
# Commit.where(
# repo: repo,
# ref: 'master',
# path: 'app/models',
# limit: 10,
# offset: 5,
# )
#
def where(options)
repo = options.delete(:repo)
raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log)
repo.log(options).map { |c| decorate(c) }
end
# Get single commit
#
# Ex.
# Commit.find(repo, '29eda46b')
#
# Commit.find(repo, 'master')
#
def find(repo, commit_id = "HEAD")
return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
obj = if commit_id.is_a?(String)
repo.rev_parse_target(commit_id)
else
Gitlab::Git::Ref.dereference_object(commit_id)
end
return nil unless obj.is_a?(Rugged::Commit)
decorate(obj)
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
nil
end
# Get last commit for HEAD
#
# Ex.
# Commit.last(repo)
#
def last(repo)
find(repo)
end
# Get last commit for specified path and ref
#
# Ex.
# Commit.last_for_path(repo, '29eda46b', 'app/models')
#
# Commit.last_for_path(repo, 'master', 'Gemfile')
#
def last_for_path(repo, ref, path = nil)
where(
repo: repo,
ref: ref,
path: path,
limit: 1
).first
end
# Get commits between two revspecs
# See also #repository.commits_between
#
# Ex.
# Commit.between(repo, '29eda46b', 'master')
#
def between(repo, base, head)
repo.commits_between(base, head).map do |commit|
decorate(commit)
end
rescue Rugged::ReferenceError
[]
end
# Delegate Repository#find_commits
def find_all(repo, options = {})
repo.find_commits(options)
end
def decorate(commit, ref = nil)
Gitlab::Git::Commit.new(commit, ref)
end
# Returns a diff object for the changes introduced by +rugged_commit+.
# If +rugged_commit+ doesn't have a parent, then the diff is between
# this commit and an empty repo. See Repository#diff for the keys
# allowed in the +options+ hash.
def diff_from_parent(rugged_commit, options = {})
options ||= {}
break_rewrites = options[:break_rewrites]
actual_options = Gitlab::Git::Diff.filter_diff_options(options)
diff = if rugged_commit.parents.empty?
rugged_commit.diff(actual_options.merge(reverse: true))
else
rugged_commit.parents[0].diff(rugged_commit, actual_options)
end
diff.find_similar!(break_rewrites: break_rewrites)
diff
end
end
def initialize(raw_commit, head = nil)
raise "Nil as raw commit passed" unless raw_commit
if raw_commit.is_a?(Hash)
init_from_hash(raw_commit)
elsif raw_commit.is_a?(Rugged::Commit)
init_from_rugged(raw_commit)
else
raise "Invalid raw commit type: #{raw_commit.class}"
end
@head = head
end
def sha
id
end
def short_id(length = 10)
id.to_s[0..length]
end
def safe_message
@safe_message ||= message
end
def created_at
committed_date
end
# Was this commit committed by a different person than the original author?
def different_committer?
author_name != committer_name || author_email != committer_email
end
def parent_id
parent_ids.first
end
# Shows the diff between the commit's parent and the commit.
#
# Cuts out the header and stats from #to_patch and returns only the diff.
def to_diff(options = {})
diff_from_parent(options).patch
end
# Returns a diff object for the changes from this commit's first parent.
# If there is no parent, then the diff is between this commit and an
# empty repo. See Repository#diff for keys allowed in the +options+
# hash.
def diff_from_parent(options = {})
Commit.diff_from_parent(raw_commit, options)
end
def has_zero_stats?
stats.total.zero?
rescue
true
end
def no_commit_message
"--no commit message"
end
def to_hash
serialize_keys.map.with_object({}) do |key, hash|
hash[key] = send(key)
end
end
def date
committed_date
end
def diffs(options = {})
Gitlab::Git::DiffCollection.new(diff_from_parent(options), options)
end
def parents
raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
end
def tree
raw_commit.tree
end
def stats
Gitlab::Git::CommitStats.new(self)
end
def to_patch(options = {})
begin
raw_commit.to_mbox(options)
rescue Rugged::InvalidError => ex
if ex.message =~ /Commit \w+ is a merge commit/
'Patch format is not currently supported for merge commits.'
end
end
end
# Get a collection of Rugged::Reference objects for this commit.
#
# Ex.
# commit.ref(repo)
#
def refs(repo)
repo.refs_hash[id]
end
# Get ref names collection
#
# Ex.
# commit.ref_names(repo)
#
def ref_names(repo)
refs(repo).map do |ref|
ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
end
end
def message
encode! @message
end
def author_name
encode! @author_name
end
def author_email
encode! @author_email
end
def committer_name
encode! @committer_name
end
def committer_email
encode! @committer_email
end
private
def init_from_hash(hash)
raw_commit = hash.symbolize_keys
serialize_keys.each do |key|
send("#{key}=", raw_commit[key])
end
end
def init_from_rugged(commit)
author = commit.author
committer = commit.committer
@raw_commit = commit
@id = commit.oid
@message = commit.message
@authored_date = author[:time]
@committed_date = committer[:time]
@author_name = author[:name]
@author_email = author[:email]
@committer_name = committer[:name]
@committer_email = committer[:email]
@parent_ids = commit.parents.map(&:oid)
end
def serialize_keys
SERIALIZE_KEYS
end
end
end
end
# Gitlab::Git::CommitStats counts the additions, deletions, and total changes
# in a commit.
module Gitlab
module Git
class CommitStats
attr_reader :id, :additions, :deletions, :total
# Instantiate a CommitStats object
def initialize(commit)
@id = commit.id
@additions = 0
@deletions = 0
@total = 0
diff = commit.diff_from_parent
diff.each_patch do |p|
# TODO: Use the new Rugged convenience methods when they're released
@additions += p.stat[0]
@deletions += p.stat[1]
@total += p.changes
end
end
end
end
end
module Gitlab
module Git
class Compare
attr_reader :head, :base, :straight
def initialize(repository, base, head, straight = false)
@repository = repository
@straight = straight
unless base && head
@commits = []
return
end
@base = Gitlab::Git::Commit.find(repository, base.try(:strip))
@head = Gitlab::Git::Commit.find(repository, head.try(:strip))
@commits = [] unless @base && @head
@commits = [] if same
end
def same
@base && @head && @base.id == @head.id
end
def commits
return @commits if defined?(@commits)
@commits = Gitlab::Git::Commit.between(@repository, @base.id, @head.id)
end
def diffs(options = {})
unless @head && @base
return Gitlab::Git::DiffCollection.new([])
end
paths = options.delete(:paths) || []
options[:straight] = @straight
Gitlab::Git::Diff.between(@repository, @head.id, @base.id, options, *paths)
end
end
end
end
# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object
module Gitlab
module Git
class Diff
class TimeoutError < StandardError; end
include Gitlab::Git::EncodingHelper
# Diff properties
attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
# Stats properties
attr_accessor :new_file, :renamed_file, :deleted_file
attr_accessor :too_large
# The maximum size of a diff to display.
DIFF_SIZE_LIMIT = 102400 # 100 KB
# The maximum size before a diff is collapsed.
DIFF_COLLAPSE_LIMIT = 10240 # 10 KB
class << self
def between(repo, head, base, options = {}, *paths)
straight = options.delete(:straight) || false
common_commit = if straight
base
else
# Only show what is new in the source branch
# compared to the target branch, not the other way
# around. The linex below with merge_base is
# equivalent to diff with three dots (git diff
# branch1...branch2) From the git documentation:
# "git diff A...B" is equivalent to "git diff
# $(git-merge-base A B) B"
repo.merge_base_commit(head, base)
end
options ||= {}
actual_options = filter_diff_options(options)
repo.diff(common_commit, head, actual_options, *paths)
end
# Return a copy of the +options+ hash containing only keys that can be
# passed to Rugged. Allowed options are:
#
# :max_size ::
# An integer specifying the maximum byte size of a file before a it
# will be treated as binary. The default value is 512MB.
#
# :context_lines ::
# The number of unchanged lines that define the boundary of a hunk
# (and to display before and after the actual changes). The default is
# 3.
#
# :interhunk_lines ::
# The maximum number of unchanged lines between hunk boundaries before
# the hunks will be merged into a one. The default is 0.
#
# :old_prefix ::
# The virtual "directory" to prefix to old filenames in hunk headers.
# The default is "a".
#
# :new_prefix ::
# The virtual "directory" to prefix to new filenames in hunk headers.
# The default is "b".
#
# :reverse ::
# If true, the sides of the diff will be reversed.
#
# :force_text ::
# If true, all files will be treated as text, disabling binary
# attributes & detection.
#
# :ignore_whitespace ::
# If true, all whitespace will be ignored.
#
# :ignore_whitespace_change ::
# If true, changes in amount of whitespace will be ignored.
#
# :ignore_whitespace_eol ::
# If true, whitespace at end of line will be ignored.
#
# :ignore_submodules ::
# if true, submodules will be excluded from the diff completely.
#
# :patience ::
# If true, the "patience diff" algorithm will be used (currenlty
# unimplemented).
#
# :include_ignored ::
# If true, ignored files will be included in the diff.
#
# :include_untracked ::
# If true, untracked files will be included in the diff.
#
# :include_unmodified ::
# If true, unmodified files will be included in the diff.
#
# :recurse_untracked_dirs ::
# Even if +:include_untracked+ is true, untracked directories will
# only be marked with a single entry in the diff. If this flag is set
# to true, all files under ignored directories will be included in the
# diff, too.
#
# :disable_pathspec_match ::
# If true, the given +*paths+ will be applied as exact matches,
# instead of as fnmatch patterns.
#
# :deltas_are_icase ::
# If true, filename comparisons will be made with case-insensitivity.
#
# :include_untracked_content ::
# if true, untracked content will be contained in the the diff patch
# text.
#
# :skip_binary_check ::
# If true, diff deltas will be generated without spending time on
# binary detection. This is useful to improve performance in cases
# where the actual file content difference is not needed.
#
# :include_typechange ::
# If true, type changes for files will not be interpreted as deletion
# of the "old file" and addition of the "new file", but will generate
# typechange records.
#
# :include_typechange_trees ::
# Even if +:include_typechange+ is true, blob -> tree changes will
# still usually be handled as a deletion of the blob. If this flag is
# set to true, blob -> tree changes will be marked as typechanges.
#
# :ignore_filemode ::
# If true, file mode changes will be ignored.
#
# :recurse_ignored_dirs ::
# Even if +:include_ignored+ is true, ignored directories will only be
# marked with a single entry in the diff. If this flag is set to true,
# all files under ignored directories will be included in the diff,
# too.
def filter_diff_options(options, default_options = {})
allowed_options = [:max_size, :context_lines, :interhunk_lines,
:old_prefix, :new_prefix, :reverse, :force_text,
:ignore_whitespace, :ignore_whitespace_change,
:ignore_whitespace_eol, :ignore_submodules,
:patience, :include_ignored, :include_untracked,
:include_unmodified, :recurse_untracked_dirs,
:disable_pathspec_match, :deltas_are_icase,
:include_untracked_content, :skip_binary_check,
:include_typechange, :include_typechange_trees,
:ignore_filemode, :recurse_ignored_dirs, :paths,
:max_files, :max_lines, :all_diffs, :no_collapse]
if default_options
actual_defaults = default_options.dup
actual_defaults.keep_if do |key|
allowed_options.include?(key)
end
else
actual_defaults = {}
end
if options
filtered_opts = options.dup
filtered_opts.keep_if do |key|
allowed_options.include?(key)
end
filtered_opts = actual_defaults.merge(filtered_opts)
else
filtered_opts = actual_defaults
end
filtered_opts
end
end
def initialize(raw_diff, collapse: false)
case raw_diff
when Hash
init_from_hash(raw_diff, collapse: collapse)
when Rugged::Patch, Rugged::Diff::Delta
init_from_rugged(raw_diff, collapse: collapse)
when nil
raise "Nil as raw diff passed"
else
raise "Invalid raw diff type: #{raw_diff.class}"
end
end
def serialize_keys
@serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large)
end
def to_hash
hash = {}
keys = serialize_keys
keys.each do |key|
hash[key] = send(key)
end
hash
end
def submodule?
a_mode == '160000' || b_mode == '160000'
end
def line_count
@line_count ||= Util.count_lines(@diff)
end
def too_large?
if @too_large.nil?
@too_large = @diff.bytesize >= DIFF_SIZE_LIMIT
else
@too_large
end
end
def collapsible?
@diff.bytesize >= DIFF_COLLAPSE_LIMIT
end
def prune_large_diff!
@diff = ''
@line_count = 0
@too_large = true
end
def collapsed?
return @collapsed if defined?(@collapsed)
false
end
def prune_collapsed_diff!
@diff = ''
@line_count = 0
@collapsed = true
end
private
def init_from_rugged(rugged, collapse: false)
if rugged.is_a?(Rugged::Patch)
init_from_rugged_patch(rugged, collapse: collapse)
d = rugged.delta
else
d = rugged
end
@new_path = encode!(d.new_file[:path])
@old_path = encode!(d.old_file[:path])
@a_mode = d.old_file[:mode].to_s(8)
@b_mode = d.new_file[:mode].to_s(8)
@new_file = d.added?
@renamed_file = d.renamed?
@deleted_file = d.deleted?
end
def init_from_rugged_patch(patch, collapse: false)
# Don't bother initializing diffs that are too large. If a diff is
# binary we're not going to display anything so we skip the size check.
return if !patch.delta.binary? && prune_large_patch(patch, collapse)
@diff = encode!(strip_diff_headers(patch.to_s))
end
def init_from_hash(hash, collapse: false)
raw_diff = hash.symbolize_keys
serialize_keys.each do |key|
send(:"#{key}=", raw_diff[key.to_sym])
end
prune_large_diff! if too_large?
prune_collapsed_diff! if collapse && collapsible?
end
# If the patch surpasses any of the diff limits it calls the appropiate
# prune method and returns true. Otherwise returns false.
def prune_large_patch(patch, collapse)
size = 0
patch.each_hunk do |hunk|
hunk.each_line do |line|
size += line.content.bytesize
if size >= DIFF_SIZE_LIMIT
prune_large_diff!
return true
end
end
end
if collapse && size >= DIFF_COLLAPSE_LIMIT
prune_collapsed_diff!
return true
end
false
end
# Strip out the information at the beginning of the patch's text to match
# Grit's output
def strip_diff_headers(diff_text)
# Delete everything up to the first line that starts with '---' or
# 'Binary'
diff_text.sub!(/\A.*?^(---|Binary)/m, '\1')
if diff_text.start_with?('---', 'Binary')
diff_text
else
# If the diff_text did not contain a line starting with '---' or
# 'Binary', return the empty string. No idea why; we are just
# preserving behavior from before the refactor.
''
end
end
end
end
end
module Gitlab
module Git
class DiffCollection
include Enumerable
DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze
def initialize(iterator, options = {})
@iterator = iterator
@max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
@max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
@max_bytes = @max_files * 5120 # Average 5 KB per file
@safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min
@safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min
@safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
@all_diffs = !!options.fetch(:all_diffs, false)
@no_collapse = !!options.fetch(:no_collapse, true)
@deltas_only = !!options.fetch(:deltas_only, false)
@line_count = 0
@byte_count = 0
@overflow = false
@array = Array.new
end
def each(&block)
if @populated
# @iterator.each is slower than just iterating the array in place
@array.each(&block)
elsif @deltas_only
each_delta(&block)
else
each_patch(&block)
end
end
def empty?
!@iterator.any?
end
def overflow?
populate!
!!@overflow
end
def size
@size ||= count # forces a loop using each method
end
def real_size
populate!
if @overflow
"#{size}+"
else
size.to_s
end
end
def decorate!
collection = each_with_index do |element, i|
@array[i] = yield(element)
end
@populated = true
collection
end
private
def populate!
return if @populated
each { nil } # force a loop through all diffs
@populated = true
nil
end
def over_safe_limits?(files)
files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes
end
def each_delta
@iterator.each_delta.with_index do |delta, i|
diff = Gitlab::Git::Diff.new(delta)
yield @array[i] = diff
end
end
def each_patch
@iterator.each_with_index do |raw, i|
# First yield cached Diff instances from @array
if @array[i]
yield @array[i]
next
end
# We have exhausted @array, time to create new Diff instances or stop.
break if @overflow
if !@all_diffs && i >= @max_files
@overflow = true
break
end
collapse = !@all_diffs && !@no_collapse
diff = Gitlab::Git::Diff.new(raw, collapse: collapse)
if collapse && over_safe_limits?(i)
diff.prune_collapsed_diff!
end
@line_count += diff.line_count
@byte_count += diff.diff.bytesize
if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes)
# This last Diff instance pushes us over the lines limit. We stop and
# discard it.
@overflow = true
break
end
yield @array[i] = diff
end
end
end
end
end
module Gitlab
module Git
module EncodingHelper
extend self
# This threshold is carefully tweaked to prevent usage of encodings detected
# by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
# we're better off sticking with utf8 encoding.
# Reason: git diff can return strings with invalid utf8 byte sequences if it
# truncates a diff in the middle of a multibyte character. In this case
# CharlockHolmes will try to guess the encoding and will likely suggest an
# obscure encoding with low confidence.
# There is a lot more info with this merge request:
# https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
ENCODING_CONFIDENCE_THRESHOLD = 40
def encode!(message)
return nil unless message.respond_to? :force_encoding
# if message is utf-8 encoding, just return it
message.force_encoding("UTF-8")
return message if message.valid_encoding?
# return message if message type is binary
detect = CharlockHolmes::EncodingDetector.detect(message)
return message.force_encoding("BINARY") if detect && detect[:type] == :binary
# force detected encoding if we have sufficient confidence.
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
message.force_encoding(detect[:encoding])
end
# encode and clean the bad chars
message.replace clean(message)
rescue
encoding = detect ? detect[:encoding] : "unknown"
"--broken encoding: #{encoding}"
end
def encode_utf8(message)
detect = CharlockHolmes::EncodingDetector.detect(message)
if detect
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
else
clean(message)
end
end
private
def clean(message)
message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
.encode("UTF-8")
.gsub("\0".encode("UTF-8"), "")
end
end
end
end
module Gitlab
module Git
class PathHelper
class << self
def normalize_path(filename)
# Strip all leading slashes so that //foo -> foo
filename[/^\/*/] = ''
# Expand relative paths (e.g. foo/../bar)
filename = Pathname.new(filename)
filename.relative_path_from(Pathname.new(''))
end
end
end
end
end
require 'open3'
module Gitlab
module Git
module Popen
def popen(cmd, path)
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
vars = { "PWD" => path }
options = { chdir: path }
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
end
[@cmd_output, @cmd_status]
end
end
end
end
module Gitlab
module Git
class Ref
include Gitlab::Git::EncodingHelper
# Branch or tag name
# without "refs/tags|heads" prefix
attr_reader :name
# Target sha.
# Usually it is commit sha but in case
# when tag reference on other tag it can be tag sha
attr_reader :target
# Dereferenced target
# Commit object to which the Ref points to
attr_reader :dereferenced_target
# Extract branch name from full ref path
#
# Ex.
# Ref.extract_branch_name('refs/heads/master') #=> 'master'
def self.extract_branch_name(str)
str.gsub(/\Arefs\/heads\//, '')
end
def self.dereference_object(object)
object = object.target while object.is_a?(Rugged::Tag::Annotation)
object
end
def initialize(repository, name, target)
encode! name
@name = name.gsub(/\Arefs\/(tags|heads)\//, '')
@dereferenced_target = Gitlab::Git::Commit.find(repository, target)
@target = if target.respond_to?(:oid)
target.oid
elsif target.respond_to?(:name)
target.name
elsif target.is_a? String
target
else
nil
end
end
end
end
end
# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
require_relative 'encoding_helper'
require_relative 'path_helper'
require 'forwardable'
require 'tempfile'
require 'forwardable'
require "rubygems/package"
module Gitlab
module Git
class Repository
extend Forwardable
include Gitlab::Git::Popen
SEARCH_CONTEXT_LINES = 3
class NoRepository < StandardError; end
class InvalidBlobName < StandardError; end
class InvalidRef < StandardError; end
# Full path to repo
attr_reader :path
# Directory name of repo
attr_reader :name
# Rugged repo object
attr_reader :rugged
# 'path' must be the path to a _bare_ git repository, e.g.
# /path/to/my-repo.git
def initialize(path)
@path = path
@name = path.split("/").last
@attributes = Gitlab::Git::Attributes.new(path)
end
# Default branch in the repository
def root_ref
@root_ref ||= discover_default_branch
end
# Alias to old method for compatibility
def raw
rugged
end
def rugged
@rugged ||= Rugged::Repository.new(path)
rescue Rugged::RepositoryError, Rugged::OSError
raise NoRepository.new('no repository for such path')
end
# Returns an Array of branch names
# sorted by name ASC
def branch_names
branches.map(&:name)
end
# Returns an Array of Branches
def branches
rugged.branches.map do |rugged_ref|
begin
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
rescue Rugged::ReferenceError
# Omit invalid branch
end
end.compact.sort_by(&:name)
end
def reload_rugged
@rugged = nil
end
# Directly find a branch with a simple name (e.g. master)
#
# force_reload causes a new Rugged repository to be instantiated
#
# This is to work around a bug in libgit2 that causes in-memory refs to
# be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
def find_branch(name, force_reload = false)
reload_rugged if force_reload
rugged_ref = rugged.branches[name]
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref
end
def local_branches
rugged.branches.each(:local).map do |branch|
Gitlab::Git::Branch.new(self, branch.name, branch.target)
end
end
# Returns the number of valid branches
def branch_count
rugged.branches.count do |ref|
begin
ref.name && ref.target # ensures the branch is valid
true
rescue Rugged::ReferenceError
false
end
end
end
# Returns an Array of tag names
def tag_names
rugged.tags.map { |t| t.name }
end
# Returns an Array of Tags
def tags
rugged.references.each("refs/tags/*").map do |ref|
message = nil
if ref.target.is_a?(Rugged::Tag::Annotation)
tag_message = ref.target.message
if tag_message.respond_to?(:chomp)
message = tag_message.chomp
end
end
Gitlab::Git::Tag.new(self, ref.name, ref.target, message)
end.sort_by(&:name)
end
# Returns true if the given tag exists
#
# name - The name of the tag as a String.
def tag_exists?(name)
!!rugged.tags[name]
end
# Returns true if the given branch exists
#
# name - The name of the branch as a String.
def branch_exists?(name)
rugged.branches.exists?(name)
# If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
# Whatever code calls this method shouldn't have to deal with that so
# instead we just return `false` (which is true since a branch doesn't
# exist when it has an invalid name).
rescue Rugged::ReferenceError
false
end
# Returns an Array of branch and tag names
def ref_names
branch_names + tag_names
end
# Deprecated. Will be removed in 5.2
def heads
rugged.references.each("refs/heads/*").map do |head|
Gitlab::Git::Ref.new(self, head.name, head.target)
end.sort_by(&:name)
end
def has_commits?
!empty?
end
def empty?
rugged.empty?
end
def bare?
rugged.bare?
end
def repo_exists?
!!rugged
end
# Discovers the default branch based on the repository's available branches
#
# - If no branches are present, returns nil
# - If one branch is present, returns its name
# - If two or more branches are present, returns current HEAD or master or first branch
def discover_default_branch
names = branch_names
return if names.empty?
return names[0] if names.length == 1
if rugged_head
extracted_name = Ref.extract_branch_name(rugged_head.name)
return extracted_name if names.include?(extracted_name)
end
if names.include?('master')
'master'
else
names[0]
end
end
def rugged_head
rugged.head
rescue Rugged::ReferenceError
nil
end
def archive_metadata(ref, storage_path, format = "tar.gz")
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
project_name = self.name.chomp('.git')
prefix = "#{project_name}-#{ref}-#{commit.id}"
{
'RepoPath' => path,
'ArchivePrefix' => prefix,
'ArchivePath' => archive_file_path(prefix, storage_path, format),
'CommitId' => commit.id,
}
end
def archive_file_path(name, storage_path, format = "tar.gz")
# Build file path
return nil unless name
extension =
case format
when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
"tar.bz2"
when "tar"
"tar"
when "zip"
"zip"
else
# everything else should fall back to tar.gz
"tar.gz"
end
file_name = "#{name}.#{extension}"
File.join(storage_path, self.name, file_name)
end
# Return repo size in megabytes
def size
size = popen(%w(du -sk), path).first.strip.to_i
(size.to_f / 1024).round(2)
end
# Returns an array of BlobSnippets for files at the specified +ref+ that
# contain the +query+ string.
def search_files(query, ref = nil)
greps = []
ref ||= root_ref
populated_index(ref).each do |entry|
# Discard submodules
next if submodule?(entry)
blob = Gitlab::Git::Blob.raw(self, entry[:oid])
# Skip binary files
next if blob.data.encoding == Encoding::ASCII_8BIT
blob.load_all_data!(self)
greps += build_greps(blob.data, query, ref, entry[:path])
end
greps
end
# Use the Rugged Walker API to build an array of commits.
#
# Usage.
# repo.log(
# ref: 'master',
# path: 'app/models',
# limit: 10,
# offset: 5,
# after: Time.new(2016, 4, 21, 14, 32, 10)
# )
#
def log(options)
default_options = {
limit: 10,
offset: 0,
path: nil,
follow: false,
skip_merges: false,
disable_walk: false,
after: nil,
before: nil
}
options = default_options.merge(options)
options[:limit] ||= 0
options[:offset] ||= 0
actual_ref = options[:ref] || root_ref
begin
sha = sha_from_ref(actual_ref)
rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
# Return an empty array if the ref wasn't found
return []
end
if log_using_shell?(options)
log_by_shell(sha, options)
else
log_by_walk(sha, options)
end
end
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
options[:skip_merges] ||
options[:after] ||
options[:before]
end
def log_by_walk(sha, options)
walk_options = {
show: sha,
sort: Rugged::SORT_DATE,
limit: options[:limit],
offset: options[:offset]
}
Rugged::Walker.walk(rugged, walk_options).to_a
end
def log_by_shell(sha, options)
cmd = %W(git --git-dir=#{path} log)
cmd += %W(-n #{options[:limit].to_i})
cmd += %w(--format=%H)
cmd += %W(--skip=#{options[:offset].to_i})
cmd += %w(--follow) if options[:follow]
cmd += %w(--no-merges) if options[:skip_merges]
cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
cmd += [sha]
cmd += %W(-- #{options[:path]}) if options[:path].present?
raw_output = IO.popen(cmd) {|io| io.read }
log = raw_output.lines.map do |c|
Rugged::Commit.new(rugged, c.strip)
end
log.is_a?(Array) ? log : []
end
def sha_from_ref(ref)
rev_parse_target(ref).oid
end
# Return the object that +revspec+ points to. If +revspec+ is an
# annotated tag, then return the tag's target instead.
def rev_parse_target(revspec)
obj = rugged.rev_parse(revspec)
Ref.dereference_object(obj)
end
# Return a collection of Rugged::Commits between the two revspec arguments.
# See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for
# a detailed list of valid arguments.
def commits_between(from, to)
walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
sha_from = sha_from_ref(from)
sha_to = sha_from_ref(to)
walker.push(sha_to)
walker.hide(sha_from)
commits = walker.to_a
walker.reset
commits
end
# Counts the amount of commits between `from` and `to`.
def count_commits_between(from, to)
commits_between(from, to).size
end
# Returns the SHA of the most recent common ancestor of +from+ and +to+
def merge_base_commit(from, to)
rugged.merge_base(from, to)
end
# Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
# split larger rewrites into delete/add pairs.
def diff(from, to, options = {}, *paths)
Gitlab::Git::DiffCollection.new(diff_patches(from, to, options, *paths), options)
end
# Returns commits collection
#
# Ex.
# repo.find_commits(
# ref: 'master',
# max_count: 10,
# skip: 5,
# order: :date
# )
#
# +options+ is a Hash of optional arguments to git
# :ref is the ref from which to begin (SHA1 or name)
# :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip
# :order is the commits order and allowed value is :date(default) or :topo
#
def find_commits(options = {})
actual_options = options.dup
allowed_options = [:ref, :max_count, :skip, :contains, :order]
actual_options.keep_if do |key|
allowed_options.include?(key)
end
default_options = { skip: 0 }
actual_options = default_options.merge(actual_options)
walker = Rugged::Walker.new(rugged)
if actual_options[:ref]
walker.push(rugged.rev_parse_oid(actual_options[:ref]))
elsif actual_options[:contains]
branches_contains(actual_options[:contains]).each do |branch|
walker.push(branch.target_id)
end
else
rugged.references.each("refs/heads/*") do |ref|
walker.push(ref.target_id)
end
end
if actual_options[:order] == :topo
walker.sorting(Rugged::SORT_TOPO)
else
walker.sorting(Rugged::SORT_DATE)
end
commits = []
offset = actual_options[:skip]
limit = actual_options[:max_count]
walker.each(offset: offset, limit: limit) do |commit|
gitlab_commit = Gitlab::Git::Commit.decorate(commit)
commits.push(gitlab_commit)
end
walker.reset
commits
rescue Rugged::OdbError
[]
end
# Returns branch names collection that contains the special commit(SHA1
# or name)
#
# Ex.
# repo.branch_names_contains('master')
#
def branch_names_contains(commit)
branches_contains(commit).map { |c| c.name }
end
# Returns branch collection that contains the special commit(SHA1 or name)
#
# Ex.
# repo.branch_names_contains('master')
#
def branches_contains(commit)
commit_obj = rugged.rev_parse(commit)
parent = commit_obj.parents.first unless commit_obj.parents.empty?
walker = Rugged::Walker.new(rugged)
rugged.branches.select do |branch|
walker.push(branch.target_id)
walker.hide(parent) if parent
result = walker.any? { |c| c.oid == commit_obj.oid }
walker.reset
result
end
end
# Get refs hash which key is SHA1
# and value is a Rugged::Reference
def refs_hash
# Initialize only when first call
if @refs_hash.nil?
@refs_hash = Hash.new { |h, k| h[k] = [] }
rugged.references.each do |r|
# Symbolic/remote references may not have an OID; skip over them
target_oid = r.target.try(:oid)
if target_oid
sha = rev_parse_target(target_oid).oid
@refs_hash[sha] << r
end
end
end
@refs_hash
end
# Lookup for rugged object by oid or ref name
def lookup(oid_or_ref_name)
rugged.rev_parse(oid_or_ref_name)
end
# Return hash with submodules info for this repository
#
# Ex.
# {
# "rack" => {
# "id" => "c67be4624545b4263184c4a0e8f887efd0a66320",
# "path" => "rack",
# "url" => "git://github.com/chneukirchen/rack.git"
# },
# "encoding" => {
# "id" => ....
# }
# }
#
def submodules(ref)
commit = rev_parse_target(ref)
return {} unless commit
begin
content = blob_content(commit, ".gitmodules")
rescue InvalidBlobName
return {}
end
parse_gitmodules(commit, content)
end
# Return total commits count accessible from passed ref
def commit_count(ref)
walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
oid = rugged.rev_parse_oid(ref)
walker.push(oid)
walker.count
end
# Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or
# tag name or a commit SHA. Valid +reset_type+ values are:
#
# [:soft]
# the head will be moved to the commit.
# [:mixed]
# will trigger a +:soft+ reset, plus the index will be replaced
# with the content of the commit tree.
# [:hard]
# will trigger a +:mixed+ reset and the working directory will be
# replaced with the content of the index. (Untracked and ignored files
# will be left alone)
def reset(ref, reset_type)
rugged.reset(ref, reset_type)
end
# Mimic the `git clean` command and recursively delete untracked files.
# Valid keys that can be passed in the +options+ hash are:
#
# :d - Remove untracked directories
# :f - Remove untracked directories that are managed by a different
# repository
# :x - Remove ignored files
#
# The value in +options+ must evaluate to true for an option to take
# effect.
#
# Examples:
#
# repo.clean(d: true, f: true) # Enable the -d and -f options
#
# repo.clean(d: false, x: true) # -x is enabled, -d is not
def clean(options = {})
strategies = [:remove_untracked]
strategies.push(:force) if options[:f]
strategies.push(:remove_ignored) if options[:x]
# TODO: implement this method
end
# Check out the specified ref. Valid options are:
#
# :b - Create a new branch at +start_point+ and set HEAD to the new
# branch.
#
# * These options are passed to the Rugged::Repository#checkout method:
#
# :progress ::
# A callback that will be executed for checkout progress notifications.
# Up to 3 parameters are passed on each execution:
#
# - The path to the last updated file (or +nil+ on the very first
# invocation).
# - The number of completed checkout steps.
# - The number of total checkout steps to be performed.
#
# :notify ::
# A callback that will be executed for each checkout notification
# types specified with +:notify_flags+. Up to 5 parameters are passed
# on each execution:
#
# - An array containing the +:notify_flags+ that caused the callback
# execution.
# - The path of the current file.
# - A hash describing the baseline blob (or +nil+ if it does not
# exist).
# - A hash describing the target blob (or +nil+ if it does not exist).
# - A hash describing the workdir blob (or +nil+ if it does not
# exist).
#
# :strategy ::
# A single symbol or an array of symbols representing the strategies
# to use when performing the checkout. Possible values are:
#
# :none ::
# Perform a dry run (default).
#
# :safe ::
# Allow safe updates that cannot overwrite uncommitted data.
#
# :safe_create ::
# Allow safe updates plus creation of missing files.
#
# :force ::
# Allow all updates to force working directory to look like index.
#
# :allow_conflicts ::
# Allow checkout to make safe updates even if conflicts are found.
#
# :remove_untracked ::
# Remove untracked files not in index (that are not ignored).
#
# :remove_ignored ::
# Remove ignored files not in index.
#
# :update_only ::
# Only update existing files, don't create new ones.
#
# :dont_update_index ::
# Normally checkout updates index entries as it goes; this stops
# that.
#
# :no_refresh ::
# Don't refresh index/config/etc before doing checkout.
#
# :disable_pathspec_match ::
# Treat pathspec as simple list of exact match file paths.
#
# :skip_locked_directories ::
# Ignore directories in use, they will be left empty.
#
# :skip_unmerged ::
# Allow checkout to skip unmerged files (NOT IMPLEMENTED).
#
# :use_ours ::
# For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED).
#
# :use_theirs ::
# For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED).
#
# :update_submodules ::
# Recursively checkout submodules with same options (NOT
# IMPLEMENTED).
#
# :update_submodules_if_changed ::
# Recursively checkout submodules if HEAD moved in super repo (NOT
# IMPLEMENTED).
#
# :disable_filters ::
# If +true+, filters like CRLF line conversion will be disabled.
#
# :dir_mode ::
# Mode for newly created directories. Default: +0755+.
#
# :file_mode ::
# Mode for newly created files. Default: +0755+ or +0644+.
#
# :file_open_flags ::
# Mode for opening files. Default:
# <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>.
#
# :notify_flags ::
# A single symbol or an array of symbols representing the cases in
# which the +:notify+ callback should be invoked. Possible values are:
#
# :none ::
# Do not invoke the +:notify+ callback (default).
#
# :conflict ::
# Invoke the callback for conflicting paths.
#
# :dirty ::
# Invoke the callback for "dirty" files, i.e. those that do not need
# an update but no longer match the baseline.
#
# :updated ::
# Invoke the callback for any file that was changed.
#
# :untracked ::
# Invoke the callback for untracked files.
#
# :ignored ::
# Invoke the callback for ignored files.
#
# :all ::
# Invoke the callback for all these cases.
#
# :paths ::
# A glob string or an array of glob strings specifying which paths
# should be taken into account for the checkout operation. +nil+ will
# match all files. Default: +nil+.
#
# :baseline ::
# A Rugged::Tree that represents the current, expected contents of the
# workdir. Default: +HEAD+.
#
# :target_directory ::
# A path to an alternative workdir directory in which the checkout
# should be performed.
def checkout(ref, options = {}, start_point = "HEAD")
if options[:b]
rugged.branches.create(ref, start_point)
options.delete(:b)
end
default_options = { strategy: [:recreate_missing, :safe] }
rugged.checkout(ref, default_options.merge(options))
end
# Delete the specified branch from the repository
def delete_branch(branch_name)
rugged.branches.delete(branch_name)
end
# Create a new branch named **ref+ based on **stat_point+, HEAD by default
#
# Examples:
# create_branch("feature")
# create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD")
rugged_ref = rugged.branches.create(ref, start_point)
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
rescue Rugged::ReferenceError => e
raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
raise InvalidRef.new("Invalid reference #{start_point}")
end
# Return an array of this repository's remote names
def remote_names
rugged.remotes.each_name.to_a
end
# Delete the specified remote from this repository.
def remote_delete(remote_name)
rugged.remotes.delete(remote_name)
end
# Add a new remote to this repository. Returns a Rugged::Remote object
def remote_add(remote_name, url)
rugged.remotes.create(remote_name, url)
end
# Update the specified remote using the values in the +options+ hash
#
# Example
# repo.update_remote("origin", url: "path/to/repo")
def remote_update(remote_name, options = {})
# TODO: Implement other remote options
rugged.remotes.set_url(remote_name, options[:url]) if options[:url]
end
# Fetch the specified remote
def fetch(remote_name)
rugged.remotes[remote_name].fetch
end
# Push +*refspecs+ to the remote identified by +remote_name+.
def push(remote_name, *refspecs)
rugged.remotes[remote_name].push(refspecs)
end
# Merge the +source_name+ branch into the +target_name+ branch. This is
# equivalent to `git merge --no_ff +source_name+`, since a merge commit
# is always created.
def merge(source_name, target_name, options = {})
our_commit = rugged.branches[target_name].target
their_commit = rugged.branches[source_name].target
raise "Invalid merge target" if our_commit.nil?
raise "Invalid merge source" if their_commit.nil?
merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts?
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
update_ref: "refs/heads/#{target_name}"
)
Rugged::Commit.create(rugged, actual_options)
end
def commits_since(from_date)
walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
rugged.references.each("refs/heads/*") do |ref|
walker.push(ref.target_id)
end
commits = []
walker.each do |commit|
break if commit.author[:time].to_date < from_date
commits.push(commit)
end
commits
end
AUTOCRLF_VALUES = {
"true" => true,
"false" => false,
"input" => :input
}.freeze
def autocrlf
AUTOCRLF_VALUES[rugged.config['core.autocrlf']]
end
def autocrlf=(value)
rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
end
# Create a new directory with a .gitkeep file. Creates
# all required nested directories (i.e. mkdir -p behavior)
#
# options should contain next structure:
# author: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# committer: {
# email: 'user@example.com',
# name: 'Test User',
# time: Time.now
# },
# commit: {
# message: 'Wow such commit',
# branch: 'master',
# update_ref: false
# }
def mkdir(path, options = {})
# Check if this directory exists; if it does, then don't bother
# adding .gitkeep file.
ref = options[:commit][:branch]
path = Gitlab::Git::PathHelper.normalize_path(path).to_s
rugged_ref = rugged.ref(ref)
raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
target_commit = rugged_ref.target
raise InvalidRef.new("Invalid target commit") if target_commit.nil?
entry = tree_entry(target_commit, path)
if entry
if entry[:type] == :blob
raise InvalidBlobName.new("Directory already exists as a file")
else
raise InvalidBlobName.new("Directory already exists")
end
end
options[:file] = {
content: '',
path: "#{path}/.gitkeep",
update: true
}
Gitlab::Git::Blob.commit(self, options)
end
# Returns result like "git ls-files" , recursive and full file path
#
# Ex.
# repo.ls_files('master')
#
def ls_files(ref)
actual_ref = ref || root_ref
begin
sha_from_ref(actual_ref)
rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
# Return an empty array if the ref wasn't found
return []
end
cmd = %W(git --git-dir=#{path} ls-tree)
cmd += %w(-r)
cmd += %w(--full-tree)
cmd += %w(--full-name)
cmd += %W(-- #{actual_ref})
raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
stuff, path = f.split("\t")
_mode, type, _sha = stuff.split(" ")
path if type == "blob"
# Contain only blob type
end
raw_output.compact
end
def copy_gitattributes(ref)
begin
commit = lookup(ref)
rescue Rugged::ReferenceError
raise InvalidRef.new("Ref #{ref} is invalid")
end
# Create the paths
info_dir_path = File.join(path, 'info')
info_attributes_path = File.join(info_dir_path, 'attributes')
begin
# Retrieve the contents of the blob
gitattributes_content = blob_content(commit, '.gitattributes')
rescue InvalidBlobName
# No .gitattributes found. Should now remove any info/attributes and return
File.delete(info_attributes_path) if File.exist?(info_attributes_path)
return
end
# Create the info directory if needed
Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path)
# Write the contents of the .gitattributes file to info/attributes
# Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8
File.open(info_attributes_path, "wb") do |file|
file.write(gitattributes_content)
end
end
# Checks if the blob should be diffable according to its attributes
def diffable?(blob)
attributes(blob.path).fetch('diff') { blob.text? }
end
# Returns the Git attributes for the given file path.
#
# See `Gitlab::Git::Attributes` for more information.
def attributes(path)
@attributes.attributes(path)
end
private
# Get the content of a blob for a given commit. If the blob is a commit
# (for submodules) then return the blob's OID.
def blob_content(commit, blob_name)
blob_entry = tree_entry(commit, blob_name)
unless blob_entry
raise InvalidBlobName.new("Invalid blob name: #{blob_name}")
end
case blob_entry[:type]
when :commit
blob_entry[:oid]
when :tree
raise InvalidBlobName.new("#{blob_name} is a tree, not a blob")
when :blob
rugged.lookup(blob_entry[:oid]).content
end
end
# Parses the contents of a .gitmodules file and returns a hash of
# submodule information.
def parse_gitmodules(commit, content)
results = {}
current = ""
content.split("\n").each do |txt|
if txt =~ /^\s*\[/
current = txt.match(/(?<=").*(?=")/)[0]
results[current] = {}
else
next unless results[current]
match_data = txt.match(/(\w+)\s*=\s*(.*)/)
next unless match_data
target = match_data[2].chomp
results[current][match_data[1]] = target
if match_data[1] == "path"
begin
results[current]["id"] = blob_content(commit, target)
rescue InvalidBlobName
results.delete(current)
end
end
end
end
results
end
# Returns true if +commit+ introduced changes to +path+, using commit
# trees to make that determination. Uses the history simplification
# rules that `git log` uses by default, where a commit is omitted if it
# is TREESAME to any parent.
#
# If the +follow+ option is true and the file specified by +path+ was
# renamed, then the path value is set to the old path.
def commit_touches_path?(commit, path, follow, walker)
entry = tree_entry(commit, path)
if commit.parents.empty?
# This is the root commit, return true if it has +path+ in its tree
return !entry.nil?
end
num_treesame = 0
commit.parents.each do |parent|
parent_entry = tree_entry(parent, path)
# Only follow the first TREESAME parent for merge commits
if num_treesame > 0
walker.hide(parent)
next
end
if entry.nil? && parent_entry.nil?
num_treesame += 1
elsif entry && parent_entry && entry[:oid] == parent_entry[:oid]
num_treesame += 1
end
end
case num_treesame
when 0
detect_rename(commit, commit.parents.first, path) if follow
true
else false
end
end
# Find the entry for +path+ in the tree for +commit+
def tree_entry(commit, path)
pathname = Pathname.new(path)
first = true
tmp_entry = nil
pathname.each_filename do |dir|
if first
tmp_entry = commit.tree[dir]
first = false
elsif tmp_entry.nil?
return nil
else
tmp_entry = rugged.lookup(tmp_entry[:oid])
return nil unless tmp_entry.type == :tree
tmp_entry = tmp_entry[dir]
end
end
tmp_entry
end
# Compare +commit+ and +parent+ for +path+. If +path+ is a file and was
# renamed in +commit+, then set +path+ to the old filename.
def detect_rename(commit, parent, path)
diff = parent.diff(commit, paths: [path], disable_pathspec_match: true)
# If +path+ is a filename, not a directory, then we should only have
# one delta. We don't need to follow renames for directories.
return nil if diff.each_delta.count > 1
delta = diff.each_delta.first
if delta.added?
full_diff = parent.diff(commit)
full_diff.find_similar!
full_diff.each_delta do |full_delta|
if full_delta.renamed? && path == full_delta.new_file[:path]
# Look for the old path in ancestors
path.replace(full_delta.old_file[:path])
end
end
end
end
def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n))
git_archive_cmd = %W(git --git-dir=#{path} archive)
# Put files into a directory before archiving
prefix = "#{archive_name(treeish)}/"
git_archive_cmd << "--prefix=#{prefix}"
# Format defaults to tar
git_archive_cmd << "--format=#{format}" if format
git_archive_cmd += %W(-- #{treeish})
open(filename, 'w') do |file|
# Create a pipe to act as the '|' in 'git archive ... | gzip'
pipe_rd, pipe_wr = IO.pipe
# Get the compression process ready to accept data from the read end
# of the pipe
compress_pid = spawn(*nice(compress_cmd), in: pipe_rd, out: file)
# The read end belongs to the compression process now; we should
# close our file descriptor for it.
pipe_rd.close
# Start 'git archive' and tell it to write into the write end of the
# pipe.
git_archive_pid = spawn(*nice(git_archive_cmd), out: pipe_wr)
# The write end belongs to 'git archive' now; close it.
pipe_wr.close
# When 'git archive' and the compression process are finished, we are
# done.
Process.waitpid(git_archive_pid)
raise "#{git_archive_cmd.join(' ')} failed" unless $?.success?
Process.waitpid(compress_pid)
raise "#{compress_cmd.join(' ')} failed" unless $?.success?
end
end
def nice(cmd)
nice_cmd = %w(nice -n 20)
unless unsupported_platform?
nice_cmd += %w(ionice -c 2 -n 7)
end
nice_cmd + cmd
end
def unsupported_platform?
%w[darwin freebsd solaris].map { |platform| RUBY_PLATFORM.include?(platform) }.any?
end
# Returns true if the index entry has the special file mode that denotes
# a submodule.
def submodule?(index_entry)
index_entry[:mode] == 57344
end
# Return a Rugged::Index that has read from the tree at +ref_name+
def populated_index(ref_name)
commit = rev_parse_target(ref_name)
index = rugged.index
index.read_tree(commit.tree)
index
end
# Return an array of BlobSnippets for lines in +file_contents+ that match
# +query+
def build_greps(file_contents, query, ref, filename)
# The file_contents string is potentially huge so we make sure to loop
# through it one line at a time. This gives Ruby the chance to GC lines
# we are not interested in.
#
# We need to do a little extra work because we are not looking for just
# the lines that matches the query, but also for the context
# (surrounding lines). We will use Enumerable#each_cons to efficiently
# loop through the lines while keeping surrounding lines on hand.
#
# First, we turn "foo\nbar\nbaz" into
# [
# [nil, -3], [nil, -2], [nil, -1],
# ['foo', 0], ['bar', 1], ['baz', 3],
# [nil, 4], [nil, 5], [nil, 6]
# ]
lines_with_index = Enumerator.new do |yielder|
# Yield fake 'before' lines for the first line of file_contents
(-SEARCH_CONTEXT_LINES..-1).each do |i|
yielder.yield [nil, i]
end
# Yield the actual file contents
count = 0
file_contents.each_line do |line|
line.chomp!
yielder.yield [line, count]
count += 1
end
# Yield fake 'after' lines for the last line of file_contents
(count + 1..count + SEARCH_CONTEXT_LINES).each do |i|
yielder.yield [nil, i]
end
end
greps = []
# Loop through consecutive blocks of lines with indexes
lines_with_index.each_cons(2 * SEARCH_CONTEXT_LINES + 1) do |line_block|
# Get the 'middle' line and index from the block
line, _ = line_block[SEARCH_CONTEXT_LINES]
next unless line && line.match(/#{Regexp.escape(query)}/i)
# Yay, 'line' contains a match!
# Get an array with just the context lines (no indexes)
match_with_context = line_block.map(&:first)
# Remove 'nil' lines in case we are close to the first or last line
match_with_context.compact!
# Get the line number (1-indexed) of the first context line
first_context_line_number = line_block[0][1] + 1
greps << Gitlab::Git::BlobSnippet.new(
ref,
match_with_context,
first_context_line_number,
filename
)
end
greps
end
# Return the Rugged patches for the diff between +from+ and +to+.
def diff_patches(from, to, options = {}, *paths)
options ||= {}
break_rewrites = options[:break_rewrites]
actual_options = Gitlab::Git::Diff.filter_diff_options(options.merge(paths: paths))
diff = rugged.diff(from, to, actual_options)
diff.find_similar!(break_rewrites: break_rewrites)
diff.each_patch
end
end
end
end
module Gitlab
module Git
class Tag < Ref
attr_reader :object_sha
def initialize(repository, name, target, message = nil)
super(repository, name, target)
@message = message
end
def message
encode! @message
end
end
end
end
module Gitlab
module Git
class Tree
include Gitlab::Git::EncodingHelper
attr_accessor :id, :root_id, :name, :path, :type,
:mode, :commit_id, :submodule_url
class << self
# Get list of tree objects
# for repository based on commit sha and path
# Uses rugged for raw objects
def where(repository, sha, path = nil)
path = nil if path == '' || path == '/'
commit = repository.lookup(sha)
root_tree = commit.tree
tree = if path
id = find_id_by_path(repository, root_tree.oid, path)
if id
repository.lookup(id)
else
[]
end
else
root_tree
end
tree.map do |entry|
new(
id: entry[:oid],
root_id: root_tree.oid,
name: entry[:name],
type: entry[:type],
mode: entry[:filemode],
path: path ? File.join(path, entry[:name]) : entry[:name],
commit_id: sha,
)
end
end
# Recursive search of tree id for path
#
# Ex.
# blog/ # oid: 1a
# app/ # oid: 2a
# models/ # oid: 3a
# views/ # oid: 4a
#
#
# Tree.find_id_by_path(repo, '1a', 'app/models') # => '3a'
#
def find_id_by_path(repository, root_id, path)
root_tree = repository.lookup(root_id)
path_arr = path.split('/')
entry = root_tree.find do |entry|
entry[:name] == path_arr[0] && entry[:type] == :tree
end
return nil unless entry
if path_arr.size > 1
path_arr.shift
find_id_by_path(repository, entry[:oid], path_arr.join('/'))
else
entry[:oid]
end
end
end
def initialize(options)
%w(id root_id name path type mode commit_id).each do |key|
self.send("#{key}=", options[key.to_sym])
end
end
def name
encode! @name
end
def dir?
type == :tree
end
def file?
type == :blob
end
def submodule?
type == :commit
end
def readme?
name =~ /^readme/i
end
def contributing?
name =~ /^contributing/i
end
end
end
end
module Gitlab
module Git
module Util
LINE_SEP = "\n".freeze
def self.count_lines(string)
case string[-1]
when nil
0
when LINE_SEP
string.count(LINE_SEP)
else
string.count(LINE_SEP) + 1
end
end
end
end
end
......@@ -15,15 +15,36 @@ RSpec.describe 'admin deploy keys', type: :feature do
expect(page).to have_content(another_deploy_key.title)
end
it 'creates new deploy key' do
describe 'create new deploy key' do
before do
visit admin_deploy_keys_path
click_link 'New Deploy Key'
fill_in 'deploy_key_title', with: 'laptop'
fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
end
it 'creates new deploy key' do
fill_deploy_key
click_button 'Create'
expect_renders_new_key
end
it 'creates new deploy key with write access' do
fill_deploy_key
check "deploy_key_can_push"
click_button "Create"
expect_renders_new_key
expect(page).to have_content('Yes')
end
def expect_renders_new_key
expect(current_path).to eq admin_deploy_keys_path
expect(page).to have_content('laptop')
end
def fill_deploy_key
fill_in 'deploy_key_title', with: 'laptop'
fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
end
end
end
......@@ -70,16 +70,15 @@ describe 'Comments', feature: true do
end
describe 'when editing a note', js: true do
it 'contains the hidden edit form' do
page.within("#note_#{note.id}") do
is_expected.to have_css('.note-edit-form', visible: false)
end
it 'there should be a hidden edit form' do
is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1)
end
describe 'editing the note' do
before do
find('.note').hover
find(".js-note-edit").click
find('.js-note-edit').click
end
it 'shows the note edit form and hide the note body' do
......@@ -90,14 +89,29 @@ describe 'Comments', feature: true do
end
end
# TODO: fix after 7.7 release
# it "should reset the edit note form textarea with the original content of the note if cancelled" do
# within(".current-note-edit-form") do
# fill_in "note[note]", with: "Some new content"
# find(".btn-cancel").click
# expect(find(".js-note-text", visible: false).text).to eq note.note
# end
# end
it 'should reset the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
expect(find('.js-note-text', visible: false).text).to eq ''
end
end
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'This is the new content'
find('.btn-save').click
end
find('.note').hover
find('.js-note-edit').click
page.within('.current-note-edit-form') do
expect(find('#note_note').value).to eq('This is the new content')
find('.js-md:first-child').click
expect(find('#note_note').value).to eq('This is the new content****')
end
end
it 'appends the edited at time to the note' do
page.within('.current-note-edit-form') do
......
......@@ -279,8 +279,15 @@ describe 'Pipelines', :feature, :js do
context 'for valid commit' do
before { fill_in('pipeline[ref]', with: 'master') }
context 'with gitlab-ci.yml' do
before { stub_ci_pipeline_to_return_yaml_file }
expect(page).to have_selector('.js-mini-pipeline-graph')
expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']")
end
context 'when clicking a graph stage' do
it 'should open a dropdown' do
find('.js-builds-dropdown-button').trigger('click')
wait_for_ajax
it 'creates a new pipeline' do
expect { click_on 'Create pipeline' }
......@@ -291,7 +298,10 @@ describe 'Pipelines', :feature, :js do
context 'without gitlab-ci.yml' do
before { click_on 'Create pipeline' }
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
wait_for_ajax
find('a.js-ci-action-icon').trigger('click')
expect(page).not_to have_content('Cancel running')
end
end
......
//= require lib/utils/custom_event_polyfill
describe('Custom Event Polyfill', () => {
it('should be defined', () => {
expect(window.CustomEvent).toBeDefined();
});
it('should create a `CustomEvent` instance', () => {
const e = new window.CustomEvent('foo');
expect(e.type).toEqual('foo');
expect(e.bubbles).toBe(false);
expect(e.cancelable).toBe(false);
expect(e.detail).toBeFalsy();
});
it('should create a `CustomEvent` instance with a `details` object', () => {
const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } });
expect(e.type).toEqual('bar');
expect(e.bubbles).toBe(false);
expect(e.cancelable).toBe(false);
expect(e.detail.foo).toEqual('bar');
});
it('should create a `CustomEvent` instance with a `bubbles` boolean', () => {
const e = new window.CustomEvent('bar', { bubbles: true });
expect(e.type).toEqual('bar');
expect(e.bubbles).toBe(true);
expect(e.cancelable).toBe(false);
expect(e.detail).toBeFalsy();
});
it('should create a `CustomEvent` instance with a `cancelable` boolean', () => {
const e = new window.CustomEvent('bar', { cancelable: true });
expect(e.type).toEqual('bar');
expect(e.bubbles).toBe(false);
expect(e.cancelable).toBe(true);
expect(e.detail).toBeFalsy();
});
});
......@@ -19,6 +19,7 @@
fixture.load(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:issues:show');
});
describe('task lists', function() {
......
......@@ -32,9 +32,9 @@
return expect(Mousetrap.pause).toHaveBeenCalled();
});
return it('removes textarea styling', function() {
$('textarea').attr('style', 'height: 400px');
$('.notes-form textarea').attr('style', 'height: 400px');
enterZen();
return expect('textarea').not.toHaveAttr('style');
return expect($('.notes-form textarea')).not.toHaveAttr('style');
});
});
describe('in use', function() {
......@@ -43,7 +43,7 @@
});
return it('exits on Escape', function() {
escapeKeydown();
return expect($('.zen-backdrop')).not.toHaveClass('fullscreen');
return expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen');
});
});
return describe('on exit', function() {
......@@ -64,15 +64,15 @@
});
enterZen = function() {
return $('.js-zen-enter').click();
return $('.notes-form .js-zen-enter').click();
};
exitZen = function() { // Ohmmmmmmm
return $('.js-zen-leave').click();
exitZen = function() {
return $('.notes-form .js-zen-leave').click();
};
escapeKeydown = function() {
return $('textarea').trigger($.Event('keydown', {
return $('.notes-form textarea').trigger($.Event('keydown', {
keyCode: 27
}));
};
......
require 'spec_helper'
describe Gitlab::Git::Attributes, seed_helper: true do
let(:path) do
File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git')
end
subject { described_class.new(path) }
describe '#attributes' do
context 'using a path with attributes' do
it 'returns the attributes as a Hash' do
expect(subject.attributes('test.txt')).to eq({ 'text' => true })
end
it 'returns a Hash containing multiple attributes' do
expect(subject.attributes('test.sh')).
to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' })
end
it 'returns a Hash containing attributes for a file with multiple extensions' do
expect(subject.attributes('test.haml.html')).
to eq({ 'gitlab-language' => 'haml' })
end
it 'returns a Hash containing attributes for a file in a directory' do
expect(subject.attributes('foo/bar.txt')).to eq({ 'foo' => true })
end
it 'returns a Hash containing attributes with query string parameters' do
expect(subject.attributes('foo.cgi')).
to eq({ 'key' => 'value?p1=v1&p2=v2' })
end
it 'returns a Hash containing the attributes for an absolute path' do
expect(subject.attributes('/test.txt')).to eq({ 'text' => true })
end
it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do
# When a path is given without a leading slash it should still match
# patterns defined with a leading slash.
expect(subject.attributes('foo.png')).
to eq({ 'gitlab-language' => 'png' })
expect(subject.attributes('/foo.png')).
to eq({ 'gitlab-language' => 'png' })
end
it 'returns an empty Hash for a defined path without attributes' do
expect(subject.attributes('bla/bla.txt')).to eq({})
end
context 'when the "binary" option is set for a path' do
it 'returns true for the "binary" option' do
expect(subject.attributes('test.binary')['binary']).to eq(true)
end
it 'returns false for the "diff" option' do
expect(subject.attributes('test.binary')['diff']).to eq(false)
end
end
end
context 'using a path without any attributes' do
it 'returns an empty Hash' do
expect(subject.attributes('test.foo')).to eq({})
end
end
end
describe '#patterns' do
it 'parses a file with entries' do
expect(subject.patterns).to be_an_instance_of(Hash)
end
it 'parses an entry that uses a tab to separate the pattern and attributes' do
expect(subject.patterns[File.join(path, '*.md')]).
to eq({ 'gitlab-language' => 'markdown' })
end
it 'stores patterns in reverse order' do
first = subject.patterns.to_a[0]
expect(first[0]).to eq(File.join(path, 'bla/bla.txt'))
end
# It's a bit hard to test for something _not_ being processed. As such we'll
# just test the number of entries.
it 'ignores any comments and empty lines' do
expect(subject.patterns.length).to eq(10)
end
it 'does not parse anything when the attributes file does not exist' do
expect(File).to receive(:exist?).
with(File.join(path, 'info/attributes')).
and_return(false)
expect(subject.patterns).to eq({})
end
end
describe '#parse_attributes' do
it 'parses a boolean attribute' do
expect(subject.parse_attributes('text')).to eq({ 'text' => true })
end
it 'parses a negated boolean attribute' do
expect(subject.parse_attributes('-text')).to eq({ 'text' => false })
end
it 'parses a key-value pair' do
expect(subject.parse_attributes('foo=bar')).to eq({ 'foo' => 'bar' })
end
it 'parses multiple attributes' do
input = 'boolean key=value -negated'
expect(subject.parse_attributes(input)).
to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false })
end
it 'parses attributes with query string parameters' do
expect(subject.parse_attributes('foo=bar?baz=1')).
to eq({ 'foo' => 'bar?baz=1' })
end
end
describe '#each_line' do
it 'iterates over every line in the attributes file' do
args = [String] * 14 # the number of lines in the file
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
end
it 'does not yield when the attributes file does not exist' do
expect(File).to receive(:exist?).
with(File.join(path, 'info/attributes')).
and_return(false)
expect { |b| subject.each_line(&b) }.not_to yield_control
end
it 'does not yield when the attributes file has an unsupported encoding' do
path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git')
attrs = described_class.new(path)
expect { |b| attrs.each_line(&b) }.not_to yield_control
end
end
end
# coding: utf-8
require "spec_helper"
describe Gitlab::Git::Blame, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:blame) do
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
end
context "each count" do
it do
data = []
blame.each do |commit, line|
data << {
commit: commit,
line: line
}
end
expect(data.size).to eq(95)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq("# Contribute to GitLab")
end
end
context "ISO-8859 encoding" do
let(:blame) do
Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
end
it 'converts to UTF-8' do
data = []
blame.each do |commit, line|
data << {
commit: commit,
line: line
}
end
expect(data.size).to eq(1)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq("Ä ü")
end
end
context "unknown encoding" do
let(:blame) do
Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
end
it 'converts to UTF-8' do
expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil)
data = []
blame.each do |commit, line|
data << {
commit: commit,
line: line
}
end
expect(data.size).to eq(1)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq(" ")
end
end
end
# encoding: UTF-8
require "spec_helper"
describe Gitlab::Git::BlobSnippet, seed_helper: true do
describe :data do
context 'empty lines' do
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
it { expect(snippet.data).to be_nil }
end
context 'present lines' do
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
it { expect(snippet.data).to eq("wow\nmuch") }
end
end
end
# encoding: utf-8
require "spec_helper"
describe Gitlab::Git::Blob, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
describe :initialize do
let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
it 'handles nil data' do
expect(blob.name).to eq('test')
expect(blob.size).to eq(nil)
expect(blob.loaded_size).to eq(nil)
end
end
describe :find do
context 'file in subdir' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
it { expect(blob.path).to eq("files/ruby/popen.rb") }
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
it { expect(blob.size).to eq(669) }
it { expect(blob.mode).to eq("100644") }
end
context 'file in root' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") }
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
it { expect(blob.name).to eq(".gitignore") }
it { expect(blob.path).to eq(".gitignore") }
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
it { expect(blob.size).to eq(241) }
it { expect(blob.mode).to eq("100644") }
it { expect(blob).not_to be_binary }
end
context 'file in root with leading slash' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") }
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
it { expect(blob.name).to eq(".gitignore") }
it { expect(blob.path).to eq(".gitignore") }
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
it { expect(blob.size).to eq(241) }
it { expect(blob.mode).to eq("100644") }
end
context 'non-exist file' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") }
it { expect(blob).to be_nil }
end
context 'six submodule' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') }
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
it { expect(blob.data).to eq('') }
it 'does not get messed up by load_all_data!' do
blob.load_all_data!(repository)
expect(blob.data).to eq('')
end
it 'does not mark the blob as binary' do
expect(blob).not_to be_binary
end
end
context 'large file' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') }
let(:blob_size) { 111803 }
it { expect(blob.size).to eq(blob_size) }
it { expect(blob.data.length).to eq(blob_size) }
it 'check that this test is sane' do
expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE
end
it 'can load all data' do
blob.load_all_data!(repository)
expect(blob.data.length).to eq(blob_size)
end
it 'marks the blob as binary' do
expect(Gitlab::Git::Blob).to receive(:new).
with(hash_including(binary: true)).
and_call_original
expect(blob).to be_binary
end
end
end
describe :raw do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
it { expect(raw_blob.size).to eq(669) }
it { expect(raw_blob.truncated?).to be_falsey }
context 'large file' do
it 'limits the size of a large file' do
blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1
buffer = Array.new(blob_size, 0)
rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join(''))
blob = Gitlab::Git::Blob.raw(repository, rugged_blob)
expect(blob.size).to eq(blob_size)
expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
expect(blob.truncated?).to be_truthy
blob.load_all_data!(repository)
expect(blob.loaded_size).to eq(blob_size)
end
end
end
describe 'encoding' do
context 'file with russian text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
it { expect(blob.name).to eq("russian.rb") }
it { expect(blob.data.lines.first).to eq("Хороший файл") }
it { expect(blob.size).to eq(23) }
it { expect(blob.truncated?).to be_falsey }
# Run it twice since data is encoded after the first run
it { expect(blob.truncated?).to be_falsey }
it { expect(blob.mode).to eq("100755") }
end
context 'file with Chinese text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
it { expect(blob.name).to eq("テスト.txt") }
it { expect(blob.data).to include("これはテスト") }
it { expect(blob.size).to eq(340) }
it { expect(blob.mode).to eq("100755") }
it { expect(blob.truncated?).to be_falsey }
end
context 'file with ISO-8859 text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") }
it { expect(blob.name).to eq("iso8859.txt") }
it { expect(blob.loaded_size).to eq(4) }
it { expect(blob.size).to eq(4) }
it { expect(blob.mode).to eq("100644") }
it { expect(blob.truncated?).to be_falsey }
end
end
describe 'mode' do
context 'file regular' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
'files/ruby/regex.rb'
)
end
it { expect(blob.name).to eq('regex.rb') }
it { expect(blob.path).to eq('files/ruby/regex.rb') }
it { expect(blob.size).to eq(1200) }
it { expect(blob.mode).to eq("100644") }
end
context 'file binary' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
'files/executables/ls'
)
end
it { expect(blob.name).to eq('ls') }
it { expect(blob.path).to eq('files/executables/ls') }
it { expect(blob.size).to eq(110080) }
it { expect(blob.mode).to eq("100755") }
end
context 'file symlink to regular' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
'files/links/ruby-style-guide.md'
)
end
it { expect(blob.name).to eq('ruby-style-guide.md') }
it { expect(blob.path).to eq('files/links/ruby-style-guide.md') }
it { expect(blob.size).to eq(31) }
it { expect(blob.mode).to eq("120000") }
end
context 'file symlink to binary' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
'files/links/touch'
)
end
it { expect(blob.name).to eq('touch') }
it { expect(blob.path).to eq('files/links/touch') }
it { expect(blob.size).to eq(20) }
it { expect(blob.mode).to eq("120000") }
end
end
describe :commit do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:commit_options) do
{
file: {
content: 'Lorem ipsum...',
path: 'documents/story.txt'
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Wow such commit',
branch: 'fix-mode'
}
}
end
let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
let(:commit) { repository.lookup(commit_sha) }
it 'should add file with commit' do
# Commit message valid
expect(commit.message).to eq('Wow such commit')
tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
# Directory was created
expect(tree[:type]).to eq(:tree)
# File was created
expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
end
describe "ref updating" do
it 'creates a commit but does not udate a ref' do
commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
commit = repository.lookup(commit_sha)
# Commit message valid
expect(commit.message).to eq('Wow such commit')
# Does not update any related ref
expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
end
end
describe 'reject updates' do
it 'should reject updates' do
commit_options[:file][:update] = false
commit_options[:file][:path] = 'files/executables/ls'
expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
end
end
describe 'file modes' do
it 'should preserve file modes with commit' do
commit_options[:file][:path] = 'files/executables/ls'
entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
expect(entry[:filemode]).to eq(0100755)
end
end
end
describe :rename do
let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
let(:rename_options) do
{
file: {
path: 'NEWCONTRIBUTING.md',
previous_path: 'CONTRIBUTING.md',
content: 'Lorem ipsum...',
update: true
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Rename readme',
branch: 'master'
}
}
end
let(:rename_options2) do
{
file: {
content: 'Lorem ipsum...',
path: 'bin/new_executable',
previous_path: 'bin/executable',
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Updates toberenamed.txt',
branch: 'master',
update_ref: false
}
}
end
it 'maintains file permissions when renaming' do
mode = 0o100755
Gitlab::Git::Blob.rename(repository, rename_options2)
expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
end
it 'renames the file with commit and not change file permissions' do
ref = rename_options[:commit][:branch]
expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
end
end
describe :remove do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:commit_options) do
{
file: {
path: 'README.md'
},
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Remove readme',
branch: 'feature'
}
}
end
let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
let(:commit) { repository.lookup(commit_sha) }
let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
it 'should remove file with commit' do
# Commit message valid
expect(commit.message).to eq('Remove readme')
# File was removed
expect(blob).to be_nil
end
end
describe :lfs_pointers do
context 'file a valid lfs pointer' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'files/lfs/image.jpg'
)
end
it { expect(blob.lfs_pointer?).to eq(true) }
it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
it { expect(blob.lfs_size).to eq("19548") }
it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
it { expect(blob.name).to eq("image.jpg") }
it { expect(blob.path).to eq("files/lfs/image.jpg") }
it { expect(blob.size).to eq(130) }
it { expect(blob.mode).to eq("100644") }
end
describe 'file an invalid lfs pointer' do
context 'with correct version header but incorrect size and oid' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'files/lfs/archive-invalid.tar'
)
end
it { expect(blob.lfs_pointer?).to eq(false) }
it { expect(blob.lfs_oid).to eq(nil) }
it { expect(blob.lfs_size).to eq(nil) }
it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") }
it { expect(blob.name).to eq("archive-invalid.tar") }
it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") }
it { expect(blob.size).to eq(43) }
it { expect(blob.mode).to eq("100644") }
end
context 'with correct version header and size but incorrect size and oid' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'files/lfs/picture-invalid.png'
)
end
it { expect(blob.lfs_pointer?).to eq(false) }
it { expect(blob.lfs_oid).to eq(nil) }
it { expect(blob.lfs_size).to eq("1575078") }
it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
it { expect(blob.name).to eq("picture-invalid.png") }
it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
it { expect(blob.size).to eq(57) }
it { expect(blob.mode).to eq("100644") }
end
context 'with correct version header and size but invalid size and oid' do
let(:blob) do
Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'files/lfs/file-invalid.zip'
)
end
it { expect(blob.lfs_pointer?).to eq(false) }
it { expect(blob.lfs_oid).to eq(nil) }
it { expect(blob.lfs_size).to eq(nil) }
it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") }
it { expect(blob.name).to eq("file-invalid.zip") }
it { expect(blob.path).to eq("files/lfs/file-invalid.zip") }
it { expect(blob.size).to eq(60) }
it { expect(blob.mode).to eq("100644") }
end
end
end
end
require "spec_helper"
describe Gitlab::Git::Branch, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
subject { repository.branches }
it { is_expected.to be_kind_of Array }
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
end
describe 'first branch' do
let(:branch) { repository.branches.first }
it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) }
it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
end
describe 'master branch' do
let(:branch) do
repository.branches.find { |branch| branch.name == 'master' }
end
it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
end
it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
end
require "spec_helper"
describe Gitlab::Git::Commit, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
let(:rugged_commit) do
repository.rugged.lookup(SeedRepo::Commit::ID)
end
describe "Commit info" do
before do
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
@committer = {
email: 'mike@smith.com',
name: "Mike Smith",
time: Time.now
}
@author = {
email: 'john@smith.com',
name: "John Smith",
time: Time.now
}
@parents = [repo.head.target]
@gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) }
@tree = @parents.first.tree
sha = Rugged::Commit.create(
repo,
author: @author,
committer: @committer,
tree: @tree,
parents: @parents,
message: "Refactoring specs",
update_ref: "HEAD"
)
@raw_commit = repo.lookup(sha)
@commit = Gitlab::Git::Commit.new(@raw_commit)
end
it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) }
it { expect(@commit.id).to eq(@raw_commit.oid) }
it { expect(@commit.sha).to eq(@raw_commit.oid) }
it { expect(@commit.safe_message).to eq(@raw_commit.message) }
it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) }
it { expect(@commit.date).to eq(@raw_commit.committer[:time]) }
it { expect(@commit.author_email).to eq(@author[:email]) }
it { expect(@commit.author_name).to eq(@author[:name]) }
it { expect(@commit.committer_name).to eq(@committer[:name]) }
it { expect(@commit.committer_email).to eq(@committer[:email]) }
it { expect(@commit.different_committer?).to be_truthy }
it { expect(@commit.parents).to eq(@gitlab_parents) }
it { expect(@commit.parent_id).to eq(@parents.first.oid) }
it { expect(@commit.no_commit_message).to eq("--no commit message") }
it { expect(@commit.tree).to eq(@tree) }
after do
# Erase the new commit so other tests get the original repo
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
end
context 'Class methods' do
describe :find do
it "should return first head commit if without params" do
expect(Gitlab::Git::Commit.last(repository).id).to eq(
repository.raw.head.target.oid
)
end
it "should return valid commit" do
expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
end
it "should return valid commit for tag" do
expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
end
it "should return nil for non-commit ids" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil
end
it "should return nil for parent of non-commit object" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil
end
it "should return nil for nonexisting ids" do
expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil
end
context 'with broken repo' do
let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) }
it 'returns nil' do
expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil
end
end
end
describe :last_for_path do
context 'no path' do
subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
describe '#id' do
subject { super().id }
it { is_expected.to eq(SeedRepo::LastCommit::ID) }
end
end
context 'path' do
subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') }
describe '#id' do
subject { super().id }
it { is_expected.to eq(SeedRepo::Commit::ID) }
end
end
context 'ref + path' do
subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
describe '#id' do
subject { super().id }
it { is_expected.to eq(SeedRepo::BigCommit::ID) }
end
end
end
describe "where" do
context 'path is empty string' do
subject do
commits = Gitlab::Git::Commit.where(
repo: repository,
ref: 'master',
path: '',
limit: 10
)
commits.map { |c| c.id }
end
it 'has 10 elements' do
expect(subject.size).to eq(10)
end
it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
end
context 'path is nil' do
subject do
commits = Gitlab::Git::Commit.where(
repo: repository,
ref: 'master',
path: nil,
limit: 10
)
commits.map { |c| c.id }
end
it 'has 10 elements' do
expect(subject.size).to eq(10)
end
it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
end
context 'ref is branch name' do
subject do
commits = Gitlab::Git::Commit.where(
repo: repository,
ref: 'master',
path: 'files',
limit: 3,
offset: 1
)
commits.map { |c| c.id }
end
it 'has 3 elements' do
expect(subject.size).to eq(3)
end
it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
end
context 'ref is commit id' do
subject do
commits = Gitlab::Git::Commit.where(
repo: repository,
ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
path: 'files',
limit: 3,
offset: 1
)
commits.map { |c| c.id }
end
it 'has 3 elements' do
expect(subject.size).to eq(3)
end
it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") }
it { is_expected.not_to include(SeedRepo::Commit::ID) }
end
context 'ref is tag' do
subject do
commits = Gitlab::Git::Commit.where(
repo: repository,
ref: 'v1.0.0',
path: 'files',
limit: 3,
offset: 1
)
commits.map { |c| c.id }
end
it 'has 3 elements' do
expect(subject.size).to eq(3)
end
it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
it { is_expected.not_to include(SeedRepo::Commit::ID) }
end
end
describe :between do
subject do
commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
commits.map { |c| c.id }
end
it 'has 1 element' do
expect(subject.size).to eq(1)
end
it { is_expected.to include(SeedRepo::Commit::ID) }
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
describe :find_all do
context 'max_count' do
subject do
commits = Gitlab::Git::Commit.find_all(
repository,
max_count: 50
)
commits.map { |c| c.id }
end
it 'has 31 elements' do
expect(subject.size).to eq(33)
end
it { is_expected.to include(SeedRepo::Commit::ID) }
it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
it { is_expected.to include(SeedRepo::FirstCommit::ID) }
end
context 'ref + max_count + skip' do
subject do
commits = Gitlab::Git::Commit.find_all(
repository,
ref: 'master',
max_count: 50,
skip: 1
)
commits.map { |c| c.id }
end
it 'has 23 elements' do
expect(subject.size).to eq(24)
end
it { is_expected.to include(SeedRepo::Commit::ID) }
it { is_expected.to include(SeedRepo::FirstCommit::ID) }
it { is_expected.not_to include(SeedRepo::LastCommit::ID) }
end
context 'contains feature + max_count' do
subject do
commits = Gitlab::Git::Commit.find_all(
repository,
contains: 'feature',
max_count: 7
)
commits.map { |c| c.id }
end
it 'has 7 elements' do
expect(subject.size).to eq(7)
end
it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) }
it { is_expected.not_to include(SeedRepo::Commit::ID) }
it { is_expected.to include(SeedRepo::BigCommit::ID) }
end
end
end
describe :init_from_rugged do
let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
subject { gitlab_commit }
describe '#id' do
subject { super().id }
it { is_expected.to eq(SeedRepo::Commit::ID) }
end
end
describe :init_from_hash do
let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
subject { commit }
describe '#id' do
subject { super().id }
it { is_expected.to eq(sample_commit_hash[:id])}
end
describe '#message' do
subject { super().message }
it { is_expected.to eq(sample_commit_hash[:message])}
end
end
describe :stats do
subject { commit.stats }
describe '#additions' do
subject { super().additions }
it { is_expected.to eq(11) }
end
describe '#deletions' do
subject { super().deletions }
it { is_expected.to eq(6) }
end
end
describe :to_diff do
subject { commit.to_diff }
it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
describe :has_zero_stats? do
it { expect(commit.has_zero_stats?).to eq(false) }
end
describe :to_patch do
subject { commit.to_patch }
it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
describe :to_hash do
let(:hash) { commit.to_hash }
subject { hash }
it { is_expected.to be_kind_of Hash }
describe '#keys' do
subject { super().keys.sort }
it { is_expected.to match(sample_commit_hash.keys.sort) }
end
end
describe :diffs do
subject { commit.diffs }
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
it { expect(subject.count).to eq(2) }
it { expect(subject.first).to be_kind_of Gitlab::Git::Diff }
end
describe :ref_names do
let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
subject { commit.ref_names(repository) }
it 'has 1 element' do
expect(subject.size).to eq(1)
end
it { is_expected.to include("master") }
it { is_expected.not_to include("feature") }
end
def sample_commit_hash
{
author_email: "dmitriy.zaporozhets@gmail.com",
author_name: "Dmitriy Zaporozhets",
authored_date: "2012-02-27 20:51:12 +0200",
committed_date: "2012-02-27 20:51:12 +0200",
committer_email: "dmitriy.zaporozhets@gmail.com",
committer_name: "Dmitriy Zaporozhets",
id: SeedRepo::Commit::ID,
message: "tree css fixes",
parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"]
}
end
end
require "spec_helper"
describe Gitlab::Git::Compare, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) }
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) }
describe :commits do
subject do
compare.commits.map(&:id)
end
it 'has 8 elements' do
expect(subject.size).to eq(8)
end
it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
context 'non-existing base ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
it { is_expected.to be_empty }
end
context 'non-existing head ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
it { is_expected.to be_empty }
end
context 'base ref is equal to head ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
it { is_expected.to be_empty }
end
context 'providing nil as base ref or head ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) }
it { is_expected.to be_empty }
end
end
describe :diffs do
subject do
compare.diffs.map(&:new_path)
end
it 'has 10 elements' do
expect(subject.size).to eq(10)
end
it { is_expected.to include('files/ruby/popen.rb') }
it { is_expected.not_to include('LICENSE') }
context 'non-existing base ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
it { is_expected.to be_empty }
end
context 'non-existing head ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
it { is_expected.to be_empty }
end
end
describe :same do
subject do
compare.same
end
it { is_expected.to eq(false) }
context 'base ref is equal to head ref' do
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
it { is_expected.to eq(true) }
end
end
describe :commits_straight do
subject do
compare_straight.commits.map(&:id)
end
it 'has 8 elements' do
expect(subject.size).to eq(8)
end
it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
end
describe :diffs_straight do
subject do
compare_straight.diffs.map(&:new_path)
end
it 'has 10 elements' do
expect(subject.size).to eq(10)
end
it { is_expected.to include('files/ruby/popen.rb') }
it { is_expected.not_to include('LICENSE') }
end
end
require 'spec_helper'
describe Gitlab::Git::DiffCollection, seed_helper: true do
subject do
Gitlab::Git::DiffCollection.new(
iterator,
max_files: max_files,
max_lines: max_lines,
all_diffs: all_diffs,
no_collapse: no_collapse
)
end
let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) }
let(:file_count) { 0 }
let(:line_length) { 1 }
let(:line_count) { 1 }
let(:max_files) { 10 }
let(:max_lines) { 100 }
let(:all_diffs) { false }
let(:no_collapse) { true }
describe '#to_a' do
subject { super().to_a }
it { is_expected.to be_kind_of ::Array }
end
describe :decorate! do
let(:file_count) { 3 }
it 'modifies the array in place' do
count = 0
subject.decorate! { |d| !d.nil? && count += 1 }
expect(subject.to_a).to eq([1, 2, 3])
expect(count).to eq(3)
end
it 'avoids future iterator iterations' do
subject.decorate! { |d| d unless d.nil? }
expect(iterator).not_to receive(:each)
subject.overflow?
end
end
context 'overflow handling' do
context 'adding few enough files' do
let(:file_count) { 3 }
context 'and few enough lines' do
let(:line_count) { 10 }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('3') }
end
it { expect(subject.size).to eq(3) }
context 'when limiting is disabled' do
let(:all_diffs) { true }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('3') }
end
it { expect(subject.size).to eq(3) }
end
end
context 'and too many lines' do
let(:line_count) { 1000 }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('0+') }
end
it { expect(subject.size).to eq(0) }
context 'when limiting is disabled' do
let(:all_diffs) { true }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('3') }
end
it { expect(subject.size).to eq(3) }
end
end
end
context 'adding too many files' do
let(:file_count) { 11 }
context 'and few enough lines' do
let(:line_count) { 1 }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('10+') }
end
it { expect(subject.size).to eq(10) }
context 'when limiting is disabled' do
let(:all_diffs) { true }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('11') }
end
it { expect(subject.size).to eq(11) }
end
end
context 'and too many lines' do
let(:line_count) { 30 }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('3+') }
end
it { expect(subject.size).to eq(3) }
context 'when limiting is disabled' do
let(:all_diffs) { true }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('11') }
end
it { expect(subject.size).to eq(11) }
end
end
end
context 'adding exactly the maximum number of files' do
let(:file_count) { 10 }
context 'and few enough lines' do
let(:line_count) { 1 }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('10') }
end
it { expect(subject.size).to eq(10) }
end
end
context 'adding too many bytes' do
let(:file_count) { 10 }
let(:line_length) { 5200 }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_truthy }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('9+') }
end
it { expect(subject.size).to eq(9) }
context 'when limiting is disabled' do
let(:all_diffs) { true }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('10') }
end
it { expect(subject.size).to eq(10) }
end
end
end
describe 'empty collection' do
subject { Gitlab::Git::DiffCollection.new([]) }
describe '#overflow?' do
subject { super().overflow? }
it { is_expected.to be_falsey }
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_truthy }
end
describe '#size' do
subject { super().size }
it { is_expected.to eq(0) }
end
describe '#real_size' do
subject { super().real_size }
it { is_expected.to eq('0')}
end
end
describe :each do
context 'when diff are too large' do
let(:collection) do
Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
end
it 'yields Diff instances even when they are too large' do
expect { |b| collection.each(&b) }.
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
end
it 'prunes diffs that are too large' do
diff = nil
collection.each do |d|
diff = d
end
expect(diff.diff).to eq('')
end
end
context 'when diff is quite large will collapse by default' do
let(:iterator) { [{ diff: 'a' * 20480 }] }
context 'when no collapse is set' do
let(:no_collapse) { true }
it 'yields Diff instances even when they are quite big' do
expect { |b| subject.each(&b) }.
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
end
it 'does not prune diffs' do
diff = nil
subject.each do |d|
diff = d
end
expect(diff.diff).not_to eq('')
end
end
context 'when no collapse is unset' do
let(:no_collapse) { false }
it 'yields Diff instances even when they are quite big' do
expect { |b| subject.each(&b) }.
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
end
it 'prunes diffs that are quite big' do
diff = nil
subject.each do |d|
diff = d
end
expect(diff.diff).to eq('')
end
context 'when go over safe limits on files' do
let(:iterator) { [ fake_diff(1, 1) ] * 4 }
before(:each) do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
end
it 'prunes diffs by default even little ones' do
subject.each_with_index do |d, i|
if i < 2
expect(d.diff).not_to eq('')
else # 90 lines
expect(d.diff).to eq('')
end
end
end
end
context 'when go over safe limits on lines' do
let(:iterator) do
[
fake_diff(1, 45),
fake_diff(1, 45),
fake_diff(1, 20480),
fake_diff(1, 1)
]
end
before(:each) do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
end
it 'prunes diffs by default even little ones' do
subject.each_with_index do |d, i|
if i < 2
expect(d.diff).not_to eq('')
else # 90 lines
expect(d.diff).to eq('')
end
end
end
end
context 'when go over safe limits on bytes' do
let(:iterator) do
[
fake_diff(1, 45),
fake_diff(1, 45),
fake_diff(1, 20480),
fake_diff(1, 1)
]
end
before(:each) do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
end
it 'prunes diffs by default even little ones' do
subject.each_with_index do |d, i|
if i < 2
expect(d.diff).not_to eq('')
else # > 80 bytes
expect(d.diff).to eq('')
end
end
end
end
end
context 'when limiting is disabled' do
let(:all_diffs) { true }
it 'yields Diff instances even when they are quite big' do
expect { |b| subject.each(&b) }.
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
end
it 'does not prune diffs' do
diff = nil
subject.each do |d|
diff = d
end
expect(diff.diff).not_to eq('')
end
end
end
end
def fake_diff(line_length, line_count)
{ 'diff' => "#{'a' * line_length}\n" * line_count }
end
end
require "spec_helper"
describe Gitlab::Git::Diff, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
before do
@raw_diff_hash = {
diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""),
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "gitlab-shell"]
\tpath = gitlab-shell
\turl = https://github.com/gitlabhq/gitlab-shell.git
+[submodule "gitlab-grack"]
+ path = gitlab-grack
+ url = https://gitlab.com/gitlab-org/gitlab-grack.git
EOT
new_path: ".gitmodules",
old_path: ".gitmodules",
a_mode: '100644',
b_mode: '100644',
new_file: false,
renamed_file: false,
deleted_file: false,
too_large: false
}
@rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
[".gitmodules"]).patches.first
end
describe '.new' do
context 'using a Hash' do
context 'with a small diff' do
let(:diff) { described_class.new(@raw_diff_hash) }
it 'initializes the diff' do
expect(diff.to_hash).to eq(@raw_diff_hash)
end
it 'does not prune the diff' do
expect(diff).not_to be_too_large
end
end
context 'using a diff that is too large' do
it 'prunes the diff' do
diff = described_class.new(diff: 'a' * 204800)
expect(diff.diff).to be_empty
expect(diff).to be_too_large
end
end
end
context 'using a Rugged::Patch' do
context 'with a small diff' do
let(:diff) { described_class.new(@rugged_diff) }
it 'initializes the diff' do
expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil))
end
it 'does not prune the diff' do
expect(diff).not_to be_too_large
end
end
context 'using a diff that is too large' do
it 'prunes the diff' do
expect_any_instance_of(String).to receive(:bytesize).
and_return(1024 * 1024 * 1024)
diff = described_class.new(@rugged_diff)
expect(diff.diff).to be_empty
expect(diff).to be_too_large
end
end
context 'using a collapsable diff that is too large' do
before do
# The patch total size is 200, with lines between 21 and 54.
# This is a quick-and-dirty way to test this. Ideally, a new patch is
# added to the test repo with a size that falls between the real limits.
stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150)
stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100)
end
it 'prunes the diff as a large diff instead of as a collapsed diff' do
diff = described_class.new(@rugged_diff, collapse: true)
expect(diff.diff).to be_empty
expect(diff).to be_too_large
expect(diff).not_to be_collapsed
end
end
context 'using a large binary diff' do
it 'does not prune the diff' do
expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?).
and_return(true)
diff = described_class.new(@rugged_diff)
expect(diff.diff).not_to be_empty
end
end
end
end
describe 'straight diffs' do
let(:options) { { straight: true } }
let(:diffs) { described_class.between(repository, 'feature', 'master', options) }
it 'has the correct size' do
expect(diffs.size).to eq(24)
end
context 'diff' do
it 'is an instance of Diff' do
expect(diffs.first).to be_kind_of(described_class)
end
it 'has the correct new_path' do
expect(diffs.first.new_path).to eq('.DS_Store')
end
it 'has the correct diff' do
expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ')
end
end
end
describe '.between' do
let(:diffs) { described_class.between(repository, 'feature', 'master') }
subject { diffs }
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
describe '#size' do
subject { super().size }
it { is_expected.to eq(1) }
end
context 'diff' do
subject { diffs.first }
it { is_expected.to be_kind_of described_class }
describe '#new_path' do
subject { super().new_path }
it { is_expected.to eq('files/ruby/feature.rb') }
end
describe '#diff' do
subject { super().diff }
it { is_expected.to include '+class Feature' }
end
end
end
describe '.filter_diff_options' do
let(:options) { { max_size: 100, invalid_opt: true } }
context "without default options" do
let(:filtered_options) { described_class.filter_diff_options(options) }
it "should filter invalid options" do
expect(filtered_options).not_to have_key(:invalid_opt)
end
end
context "with default options" do
let(:filtered_options) do
default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true }
described_class.filter_diff_options(options, default_options)
end
it "should filter invalid options" do
expect(filtered_options).not_to have_key(:invalid_opt)
expect(filtered_options).not_to have_key(:bad_opt)
end
it "should merge with default options" do
expect(filtered_options).to have_key(:ignore_whitespace)
end
it "should override default options" do
expect(filtered_options).to have_key(:max_size)
expect(filtered_options[:max_size]).to eq(100)
end
end
end
describe '#submodule?' do
before do
commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
@diffs = commit.parents[0].diff(commit).patches
end
it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) }
it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) }
end
describe '#line_count' do
it 'returns the correct number of lines' do
diff = described_class.new(@rugged_diff)
expect(diff.line_count).to eq(9)
end
end
describe '#too_large?' do
it 'returns true for a diff that is too large' do
diff = described_class.new(diff: 'a' * 204800)
expect(diff.too_large?).to eq(true)
end
it 'returns false for a diff that is small enough' do
diff = described_class.new(diff: 'a')
expect(diff.too_large?).to eq(false)
end
it 'returns true for a diff that was explicitly marked as being too large' do
diff = described_class.new(diff: 'a')
diff.prune_large_diff!
expect(diff.too_large?).to eq(true)
end
end
describe '#collapsed?' do
it 'returns false by default even on quite big diff' do
diff = described_class.new(diff: 'a' * 20480)
expect(diff).not_to be_collapsed
end
it 'returns false by default for a diff that is small enough' do
diff = described_class.new(diff: 'a')
expect(diff).not_to be_collapsed
end
it 'returns true for a diff that was explicitly marked as being collapsed' do
diff = described_class.new(diff: 'a')
diff.prune_collapsed_diff!
expect(diff).to be_collapsed
end
end
describe '#collapsible?' do
it 'returns true for a diff that is quite large' do
diff = described_class.new(diff: 'a' * 20480)
expect(diff).to be_collapsible
end
it 'returns false for a diff that is small enough' do
diff = described_class.new(diff: 'a')
expect(diff).not_to be_collapsible
end
end
describe '#prune_collapsed_diff!' do
it 'prunes the diff' do
diff = described_class.new(diff: "foo\nbar")
diff.prune_collapsed_diff!
expect(diff.diff).to eq('')
expect(diff.line_count).to eq(0)
end
end
end
require "spec_helper"
describe Gitlab::Git::EncodingHelper do
let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } }
let(:binary_string) { File.join(SEED_REPOSITORY_PATH, 'gitlab_logo.png') }
describe '#encode!' do
[
[
'leaves ascii only string as is',
'ascii only string',
'ascii only string'
],
[
'leaves valid utf8 string as is',
'multibyte string №∑∉',
'multibyte string №∑∉'
],
[
'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.',
"mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'),
"mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ",
],
].each do |description, test_string, xpect|
it description do
expect(ext_class.encode!(test_string)).to eq(xpect)
end
end
it 'leaves binary string as is' do
expect(ext_class.encode!(binary_string)).to eq(binary_string)
end
end
describe '#encode_utf8' do
[
[
"encodes valid utf8 encoded string to utf8",
"λ, λ, λ".encode("UTF-8"),
"λ, λ, λ".encode("UTF-8"),
],
[
"encodes valid ASCII-8BIT encoded string to utf8",
"ascii only".encode("ASCII-8BIT"),
"ascii only".encode("UTF-8"),
],
[
"encodes valid ISO-8859-1 encoded string to utf8",
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"),
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"),
],
].each do |description, test_string, xpect|
it description do
r = ext_class.encode_utf8(test_string.force_encoding('UTF-8'))
expect(r).to eq(xpect)
expect(r.encoding.name).to eq('UTF-8')
end
end
end
describe '#clean' do
[
[
'leaves ascii only string as is',
'ascii only string',
'ascii only string'
],
[
'leaves valid utf8 string as is',
'multibyte string №∑∉',
'multibyte string №∑∉'
],
[
'removes invalid bytes from ASCII-8bit encoded multibyte string.',
"Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
"Lorem ipsum\n dolor sit amet, xyàyùabcdùefg",
],
].each do |description, test_string, xpect|
it description do
expect(ext_class.encode!(test_string)).to eq(xpect)
end
end
end
end
require "spec_helper"
describe Gitlab::Git::Repository, seed_helper: true do
include Gitlab::Git::EncodingHelper
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
describe "Respond to" do
subject { repository }
it { is_expected.to respond_to(:raw) }
it { is_expected.to respond_to(:rugged) }
it { is_expected.to respond_to(:root_ref) }
it { is_expected.to respond_to(:tags) }
end
describe "#discover_default_branch" do
let(:master) { 'master' }
let(:feature) { 'feature' }
let(:feature2) { 'feature2' }
it "returns 'master' when master exists" do
expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
expect(repository.discover_default_branch).to eq('master')
end
it "returns non-master when master exists but default branch is set to something else" do
File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature')
expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
expect(repository.discover_default_branch).to eq('feature')
File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master')
end
it "returns a non-master branch when only one exists" do
expect(repository).to receive(:branch_names).at_least(:once).and_return([feature])
expect(repository.discover_default_branch).to eq('feature')
end
it "returns a non-master branch when more than one exists and master does not" do
expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2])
expect(repository.discover_default_branch).to eq('feature')
end
it "returns nil when no branch exists" do
expect(repository).to receive(:branch_names).at_least(:once).and_return([])
expect(repository.discover_default_branch).to be_nil
end
end
describe :branch_names do
subject { repository.branch_names }
it 'has SeedRepo::Repo::BRANCHES.size elements' do
expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size)
end
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
end
describe :tag_names do
subject { repository.tag_names }
it { is_expected.to be_kind_of Array }
it 'has SeedRepo::Repo::TAGS.size elements' do
expect(subject.size).to eq(SeedRepo::Repo::TAGS.size)
end
describe '#last' do
subject { super().last }
it { is_expected.to eq("v1.2.1") }
end
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
end
shared_examples 'archive check' do |extenstion|
it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) }
it { expect(metadata['ArchivePath']).to end_with extenstion }
end
describe :archive do
let(:metadata) { repository.archive_metadata('master', '/tmp') }
it_should_behave_like 'archive check', '.tar.gz'
end
describe :archive_zip do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') }
it_should_behave_like 'archive check', '.zip'
end
describe :archive_bz2 do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') }
it_should_behave_like 'archive check', '.tar.bz2'
end
describe :archive_fallback do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') }
it_should_behave_like 'archive check', '.tar.gz'
end
describe :size do
subject { repository.size }
it { is_expected.to be < 2 }
end
describe :has_commits? do
it { expect(repository.has_commits?).to be_truthy }
end
describe :empty? do
it { expect(repository.empty?).to be_falsey }
end
describe :bare? do
it { expect(repository.bare?).to be_truthy }
end
describe :heads do
let(:heads) { repository.heads }
subject { heads }
it { is_expected.to be_kind_of Array }
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
end
context :head do
subject { heads.first }
describe '#name' do
subject { super().name }
it { is_expected.to eq("feature") }
end
context :commit do
subject { heads.first.dereferenced_target.sha }
it { is_expected.to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
end
end
end
describe :ref_names do
let(:ref_names) { repository.ref_names }
subject { ref_names }
it { is_expected.to be_kind_of Array }
describe '#first' do
subject { super().first }
it { is_expected.to eq('feature') }
end
describe '#last' do
subject { super().last }
it { is_expected.to eq('v1.2.1') }
end
end
describe :search_files do
let(:results) { repository.search_files('rails', 'master') }
subject { results }
it { is_expected.to be_kind_of Array }
describe '#first' do
subject { super().first }
it { is_expected.to be_kind_of Gitlab::Git::BlobSnippet }
end
context 'blob result' do
subject { results.first }
describe '#ref' do
subject { super().ref }
it { is_expected.to eq('master') }
end
describe '#filename' do
subject { super().filename }
it { is_expected.to eq('CHANGELOG') }
end
describe '#startline' do
subject { super().startline }
it { is_expected.to eq(35) }
end
describe '#data' do
subject { super().data }
it { is_expected.to include "Ability to filter by multiple labels" }
end
end
end
context :submodules do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
context 'where repo has submodules' do
let(:submodules) { repository.submodules('master') }
let(:submodule) { submodules.first }
it { expect(submodules).to be_kind_of Hash }
it { expect(submodules.empty?).to be_falsey }
it 'should have valid data' do
expect(submodule).to eq([
"six", {
"id" => "409f37c4f05865e4fb208c771485f211a22c4c2d",
"path" => "six",
"url" => "git://github.com/randx/six.git"
}
])
end
it 'should handle nested submodules correctly' do
nested = submodules['nested/six']
expect(nested['path']).to eq('nested/six')
expect(nested['url']).to eq('git://github.com/randx/six.git')
expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196')
end
it 'should handle deeply nested submodules correctly' do
nested = submodules['deeper/nested/six']
expect(nested['path']).to eq('deeper/nested/six')
expect(nested['url']).to eq('git://github.com/randx/six.git')
expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196')
end
it 'should not have an entry for an invalid submodule' do
expect(submodules).not_to have_key('invalid/path')
end
it 'should not have an entry for an uncommited submodule dir' do
submodules = repository.submodules('fix-existing-submodule-dir')
expect(submodules).not_to have_key('submodule-existing-dir')
end
it 'should handle tags correctly' do
submodules = repository.submodules('v1.2.1')
expect(submodules.first).to eq([
"six", {
"id" => "409f37c4f05865e4fb208c771485f211a22c4c2d",
"path" => "six",
"url" => "git://github.com/randx/six.git"
}
])
end
end
context 'where repo doesn\'t have submodules' do
let(:submodules) { repository.submodules('6d39438') }
it 'should return an empty hash' do
expect(submodules).to be_empty
end
end
end
describe :commit_count do
it { expect(repository.commit_count("master")).to eq(25) }
it { expect(repository.commit_count("feature")).to eq(9) }
end
describe "#reset" do
change_path = File.join(TEST_NORMAL_REPO_PATH, "CHANGELOG")
untracked_path = File.join(TEST_NORMAL_REPO_PATH, "UNTRACKED")
tracked_path = File.join(TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb")
change_text = "New changelog text"
untracked_text = "This file is untracked"
reset_commit = SeedRepo::LastCommit::ID
context "--hard" do
before(:all) do
# Modify a tracked file
File.open(change_path, "w") do |f|
f.write(change_text)
end
# Add an untracked file to the working directory
File.open(untracked_path, "w") do |f|
f.write(untracked_text)
end
@normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
@normal_repo.reset("HEAD", :hard)
end
it "should replace the working directory with the content of the index" do
File.open(change_path, "r") do |f|
expect(f.each_line.first).not_to eq(change_text)
end
File.open(tracked_path, "r") do |f|
expect(f.each_line.to_a[8]).to include('raise RuntimeError, "System commands')
end
end
it "should not touch untracked files" do
expect(File.exist?(untracked_path)).to be_truthy
end
it "should move the HEAD to the correct commit" do
new_head = @normal_repo.rugged.head.target.oid
expect(new_head).to eq(reset_commit)
end
it "should move the tip of the master branch to the correct commit" do
new_tip = @normal_repo.rugged.references["refs/heads/master"].
target.oid
expect(new_tip).to eq(reset_commit)
end
after(:all) do
# Fast-forward to the original HEAD
FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
ensure_seeds
end
end
end
describe "#checkout" do
new_branch = "foo_branch"
context "-b" do
before(:all) do
@normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
@normal_repo.checkout(new_branch, { b: true }, "origin/feature")
end
it "should create a new branch" do
expect(@normal_repo.rugged.branches[new_branch]).not_to be_nil
end
it "should move the HEAD to the correct commit" do
expect(@normal_repo.rugged.head.target.oid).to(
eq(@normal_repo.rugged.branches["origin/feature"].target.oid)
)
end
it "should refresh the repo's #heads collection" do
head_names = @normal_repo.heads.map { |h| h.name }
expect(head_names).to include(new_branch)
end
after(:all) do
FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
ensure_seeds
end
end
context "without -b" do
context "and specifying a nonexistent branch" do
it "should not do anything" do
normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
expect { normal_repo.checkout(new_branch) }.to raise_error(Rugged::ReferenceError)
expect(normal_repo.rugged.branches[new_branch]).to be_nil
expect(normal_repo.rugged.head.target.oid).to(
eq(normal_repo.rugged.branches["master"].target.oid)
)
head_names = normal_repo.heads.map { |h| h.name }
expect(head_names).not_to include(new_branch)
end
after(:all) do
FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
ensure_seeds
end
end
context "and with a valid branch" do
before(:all) do
@normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
@normal_repo.rugged.branches.create("feature", "origin/feature")
@normal_repo.checkout("feature")
end
it "should move the HEAD to the correct commit" do
expect(@normal_repo.rugged.head.target.oid).to(
eq(@normal_repo.rugged.branches["feature"].target.oid)
)
end
it "should update the working directory" do
File.open(File.join(TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f|
expect(f.read.each_line.to_a).not_to include(".DS_Store\n")
end
end
after(:all) do
FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
ensure_seeds
end
end
end
end
describe "#delete_branch" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
@repo.delete_branch("feature")
end
it "should remove the branch from the repo" do
expect(@repo.rugged.branches["feature"]).to be_nil
end
it "should update the repo's #heads collection" do
expect(@repo.heads).not_to include("feature")
end
after(:all) do
FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
describe "#create_branch" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
end
it "should create a new branch" do
expect(@repo.create_branch('new_branch', 'master')).not_to be_nil
end
it "should create a new branch with the right name" do
expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch')
end
it "should fail if we create an existing branch" do
@repo.create_branch('duplicated_branch', 'master')
expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
end
it "should fail if we create a branch from a non existing ref" do
expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
end
after(:all) do
FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
describe "#remote_names" do
let(:remotes) { repository.remote_names }
it "should have one entry: 'origin'" do
expect(remotes.size).to eq(1)
expect(remotes.first).to eq("origin")
end
end
describe "#refs_hash" do
let(:refs) { repository.refs_hash }
it "should have as many entries as branches and tags" do
expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
# We flatten in case a commit is pointed at by more than one branch and/or tag
expect(refs.values.flatten.size).to eq(expected_refs.size)
end
end
describe "#remote_delete" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
@repo.remote_delete("expendable")
end
it "should remove the remote" do
expect(@repo.rugged.remotes).not_to include("expendable")
end
after(:all) do
FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
describe "#remote_add" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
@repo.remote_add("new_remote", SeedHelper::GITLAB_URL)
end
it "should add the remote" do
expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote")
end
after(:all) do
FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
describe "#remote_update" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
@repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
end
it "should add the remote" do
expect(@repo.rugged.remotes["expendable"].url).to(
eq(TEST_NORMAL_REPO_PATH)
)
end
after(:all) do
FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
describe "#log" do
commit_with_old_name = nil
commit_with_new_name = nil
rename_commit = nil
before(:all) do
# Add new commits so that there's a renamed file in the commit history
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
commit_with_old_name = new_commit_edit_old_file(repo)
rename_commit = new_commit_move_file(repo)
commit_with_new_name = new_commit_edit_new_file(repo)
end
context "where 'follow' == true" do
options = { ref: "master", follow: true }
context "and 'path' is a directory" do
let(:log_commits) do
repository.log(options.merge(path: "encoding"))
end
it "should not follow renames" do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name)
end
end
context "and 'path' is a file that matches the new filename" do
let(:log_commits) do
repository.log(options.merge(path: "encoding/CHANGELOG"))
end
it "should follow renames" do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
context "and 'path' is a file that matches the old filename" do
let(:log_commits) do
repository.log(options.merge(path: "CHANGELOG"))
end
it "should not follow renames" do
expect(log_commits).to include(commit_with_old_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_new_name)
end
end
context "unknown ref" do
let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
it "should return empty" do
expect(log_commits).to eq([])
end
end
end
context "where 'follow' == false" do
options = { follow: false }
context "and 'path' is a directory" do
let(:log_commits) do
repository.log(options.merge(path: "encoding"))
end
it "should not follow renames" do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name)
end
end
context "and 'path' is a file that matches the new filename" do
let(:log_commits) do
repository.log(options.merge(path: "encoding/CHANGELOG"))
end
it "should not follow renames" do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name)
end
end
context "and 'path' is a file that matches the old filename" do
let(:log_commits) do
repository.log(options.merge(path: "CHANGELOG"))
end
it "should not follow renames" do
expect(log_commits).to include(commit_with_old_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_new_name)
end
end
context "and 'path' includes a directory that used to be a file" do
let(:log_commits) do
repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
end
it "should return a list of commits" do
expect(log_commits.size).to eq(1)
end
end
end
context "compare results between log_by_walk and log_by_shell" do
let(:options) { { ref: "master" } }
let(:commits_by_walk) { repository.log(options).map(&:oid) }
let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:oid) }
it { expect(commits_by_walk).to eq(commits_by_shell) }
context "with limit" do
let(:options) { { ref: "master", limit: 1 } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
context "with offset" do
let(:options) { { ref: "master", offset: 1 } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
context "with skip_merges" do
let(:options) { { ref: "master", skip_merges: true } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
context "with path" do
let(:options) { { ref: "master", path: "encoding" } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
context "with follow" do
let(:options) { { ref: "master", path: "encoding", follow: true } }
it { expect(commits_by_walk).to eq(commits_by_shell) }
end
end
end
context "where provides 'after' timestamp" do
options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
it "should returns commits on or after that timestamp" do
commits = repository.log(options)
expect(commits.size).to be > 0
satisfy do
commits.all? { |commit| commit.created_at >= options[:after] }
end
end
end
context "where provides 'before' timestamp" do
options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
it "should returns commits on or before that timestamp" do
commits = repository.log(options)
expect(commits.size).to be > 0
satisfy do
commits.all? { |commit| commit.created_at <= options[:before] }
end
end
end
after(:all) do
# Erase our commits so other tests get the original repo
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
end
describe "#commits_between" do
context 'two SHAs' do
let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' }
it 'returns the number of commits between' do
expect(repository.commits_between(first_sha, second_sha).count).to eq(3)
end
end
context 'SHA and master branch' do
let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
let(:branch) { 'master' }
it 'returns the number of commits between a sha and a branch' do
expect(repository.commits_between(sha, branch).count).to eq(5)
end
it 'returns the number of commits between a branch and a sha' do
expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch
end
end
context 'two branches' do
let(:first_branch) { 'feature' }
let(:second_branch) { 'master' }
it 'returns the number of commits between' do
expect(repository.commits_between(first_branch, second_branch).count).to eq(17)
end
end
end
describe '#count_commits_between' do
subject { repository.count_commits_between('feature', 'master') }
it { is_expected.to eq(17) }
end
describe "branch_names_contains" do
subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) }
it { is_expected.to include('master') }
it { is_expected.not_to include('feature') }
it { is_expected.not_to include('fix') }
end
describe '#autocrlf' do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
@repo.rugged.config['core.autocrlf'] = true
end
it 'return the value of the autocrlf option' do
expect(@repo.autocrlf).to be(true)
end
after(:all) do
@repo.rugged.config.delete('core.autocrlf')
end
end
describe '#autocrlf=' do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
@repo.rugged.config['core.autocrlf'] = false
end
it 'should set the autocrlf option to the provided option' do
@repo.autocrlf = :input
File.open(File.join(TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file|
expect(config_file.read).to match('autocrlf = input')
end
end
after(:all) do
@repo.rugged.config.delete('core.autocrlf')
end
end
describe '#find_branch' do
it 'should return a Branch for master' do
branch = repository.find_branch('master')
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
it 'should handle non-existent branch' do
branch = repository.find_branch('this-is-garbage')
expect(branch).to eq(nil)
end
it 'should reload Rugged::Repository and return master' do
expect(Rugged::Repository).to receive(:new).twice.and_call_original
repository.find_branch('master')
branch = repository.find_branch('master', force_reload: true)
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
end
describe '#branches with deleted branch' do
before(:each) do
ref = double()
allow(ref).to receive(:name) { 'bad-branch' }
allow(ref).to receive(:target) { raise Rugged::ReferenceError }
allow(repository.rugged).to receive(:branches) { [ref] }
end
it 'should return empty branches' do
expect(repository.branches).to eq([])
end
end
describe '#branch_count' do
before(:each) do
valid_ref = double(:ref)
invalid_ref = double(:ref)
allow(valid_ref).to receive_messages(name: 'master', target: double(:target))
allow(invalid_ref).to receive_messages(name: 'bad-branch')
allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError }
allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref])
end
it 'returns the number of branches' do
expect(repository.branch_count).to eq(1)
end
end
describe '#mkdir' do
let(:commit_options) do
{
author: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
committer: {
email: 'user@example.com',
name: 'Test User',
time: Time.now
},
commit: {
message: 'Test message',
branch: 'refs/heads/fix',
}
}
end
def generate_diff_for_path(path)
"diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/#{path}/.gitkeep\n"
end
shared_examples 'mkdir diff check' do |path, expected_path|
it 'creates a directory' do
result = repository.mkdir(path, commit_options)
expect(result).not_to eq(nil)
# Verify another mkdir doesn't create a directory that already exists
expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists')
end
end
describe 'creates a directory in root directory' do
it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir'
end
describe 'creates a directory in subdirectory' do
it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test'
end
describe 'creates a directory in subdirectory with a slash' do
it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2'
end
describe 'creates a directory in subdirectory with multiple slashes' do
it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3'
end
describe 'handles relative paths' do
it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative'
end
describe 'creates nested directories' do
it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test'
end
it 'does not attempt to create a directory with invalid relative path' do
expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path')
end
it 'does not attempt to overwrite a file' do
expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file')
end
it 'does not attempt to overwrite a directory' do
expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists')
end
end
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
it "read every file paths of master branch" do
expect(master_file_paths.length).to equal(40)
end
it "reads full file paths of master branch" do
expect(master_file_paths).to include("files/html/500.html")
end
it "dose not read submodule directory and empty directory of master branch" do
expect(master_file_paths).not_to include("six")
end
it "does not include 'nil'" do
expect(master_file_paths).not_to include(nil)
end
it "returns empty array when not existed branch" do
expect(not_existed_branch.length).to equal(0)
end
end
describe "#copy_gitattributes" do
let(:attributes_path) { File.join(TEST_REPO_PATH, 'info/attributes') }
it "raises an error with invalid ref" do
expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
end
context "with no .gitattrbutes" do
before(:each) do
repository.copy_gitattributes("master")
end
it "does not have an info/attributes" do
expect(File.exist?(attributes_path)).to be_falsey
end
after(:each) do
FileUtils.rm_rf(attributes_path)
end
end
context "with .gitattrbutes" do
before(:each) do
repository.copy_gitattributes("gitattributes")
end
it "has an info/attributes" do
expect(File.exist?(attributes_path)).to be_truthy
end
it "has the same content in info/attributes as .gitattributes" do
contents = File.open(attributes_path, "rb") { |f| f.read }
expect(contents).to eq("*.md binary\n")
end
after(:each) do
FileUtils.rm_rf(attributes_path)
end
end
context "with updated .gitattrbutes" do
before(:each) do
repository.copy_gitattributes("gitattributes")
repository.copy_gitattributes("gitattributes-updated")
end
it "has an info/attributes" do
expect(File.exist?(attributes_path)).to be_truthy
end
it "has the updated content in info/attributes" do
contents = File.read(attributes_path)
expect(contents).to eq("*.txt binary\n")
end
after(:each) do
FileUtils.rm_rf(attributes_path)
end
end
context "with no .gitattrbutes in HEAD but with previous info/attributes" do
before(:each) do
repository.copy_gitattributes("gitattributes")
repository.copy_gitattributes("master")
end
it "does not have an info/attributes" do
expect(File.exist?(attributes_path)).to be_falsey
end
after(:each) do
FileUtils.rm_rf(attributes_path)
end
end
end
describe '#diffable' do
info_dir_path = attributes_path = File.join(TEST_REPO_PATH, 'info')
attributes_path = File.join(info_dir_path, 'attributes')
before(:all) do
FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
File.write(attributes_path, "*.md -diff\n")
end
it "should return true for files which are text and do not have attributes" do
blob = Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'LICENSE'
)
expect(repository.diffable?(blob)).to be_truthy
end
it "should return false for binary files which do not have attributes" do
blob = Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'files/images/logo-white.png'
)
expect(repository.diffable?(blob)).to be_falsey
end
it "should return false for text files which have been marked as not being diffable in attributes" do
blob = Gitlab::Git::Blob.find(
repository,
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
'README.md'
)
expect(repository.diffable?(blob)).to be_falsey
end
after(:all) do
FileUtils.rm_rf(info_dir_path)
end
end
describe '#tag_exists?' do
it 'returns true for an existing tag' do
tag = repository.tag_names.first
expect(repository.tag_exists?(tag)).to eq(true)
end
it 'returns false for a non-existing tag' do
expect(repository.tag_exists?('v9000')).to eq(false)
end
end
describe '#branch_exists?' do
it 'returns true for an existing branch' do
expect(repository.branch_exists?('master')).to eq(true)
end
it 'returns false for a non-existing branch' do
expect(repository.branch_exists?('kittens')).to eq(false)
end
it 'returns false when using an invalid branch name' do
expect(repository.branch_exists?('.bla')).to eq(false)
end
end
describe '#local_branches' do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
end
after(:all) do
FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
it 'returns the local branches' do
create_remote_branch('joe', 'remote_branch', 'master')
@repo.create_branch('local_branch', 'master')
expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
end
end
def create_remote_branch(remote_name, branch_name, source_branch_name)
source_branch = @repo.branches.find { |branch| branch.name == source_branch_name }
rugged = @repo.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
end
# Build the options hash that's passed to Rugged::Commit#create
def commit_options(repo, index, message)
options = {}
options[:tree] = index.write_tree(repo)
options[:author] = {
email: "test@example.com",
name: "Test Author",
time: Time.gm(2014, "mar", 3, 20, 15, 1)
}
options[:committer] = {
email: "test@example.com",
name: "Test Author",
time: Time.gm(2014, "mar", 3, 20, 15, 1)
}
options[:message] ||= message
options[:parents] = repo.empty? ? [] : [repo.head.target].compact
options[:update_ref] = "HEAD"
options
end
# Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
# contents of CHANGELOG with a single new line of text.
def new_commit_edit_old_file(repo)
oid = repo.write("I replaced the changelog with this text", :blob)
index = repo.index
index.read_tree(repo.head.target.tree)
index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
options = commit_options(
repo,
index,
"Edit CHANGELOG in its original location"
)
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
# Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
# contents of encoding/CHANGELOG with new text.
def new_commit_edit_new_file(repo)
oid = repo.write("I'm a new changelog with different text", :blob)
index = repo.index
index.read_tree(repo.head.target.tree)
index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
options = commit_options(repo, index, "Edit encoding/CHANGELOG")
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
# Writes a new commit to the repo and returns a Rugged::Commit. Moves the
# CHANGELOG file to the encoding/ directory.
def new_commit_move_file(repo)
blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
file_content = repo.lookup(blob_oid).content
oid = repo.write(file_content, :blob)
index = repo.index
index.read_tree(repo.head.target.tree)
index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
index.remove("CHANGELOG")
options = commit_options(repo, index, "Move CHANGELOG to encoding/")
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
end
require "spec_helper"
describe Gitlab::Git::Tag, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
describe 'first tag' do
let(:tag) { repository.tags.first }
it { expect(tag.name).to eq("v1.0.0") }
it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") }
it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") }
it { expect(tag.message).to eq("Release") }
end
describe 'last tag' do
let(:tag) { repository.tags.last }
it { expect(tag.name).to eq("v1.2.1") }
it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") }
it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") }
it { expect(tag.message).to eq("Version 1.2.1") }
end
it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
end
require "spec_helper"
describe Gitlab::Git::Tree, seed_helper: true do
context :repo do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
it { expect(tree).to be_kind_of Array }
it { expect(tree.empty?).to be_falsey }
it { expect(tree.select(&:dir?).size).to eq(2) }
it { expect(tree.select(&:file?).size).to eq(10) }
it { expect(tree.select(&:submodule?).size).to eq(2) }
describe :dir do
let(:dir) { tree.select(&:dir?).first }
it { expect(dir).to be_kind_of Gitlab::Git::Tree }
it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') }
it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(dir.name).to eq('encoding') }
it { expect(dir.path).to eq('encoding') }
context :subdir do
let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
it { expect(subdir).to be_kind_of Gitlab::Git::Tree }
it { expect(subdir.id).to eq('a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba') }
it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(subdir.name).to eq('html') }
it { expect(subdir.path).to eq('files/html') }
end
context :subdir_file do
let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree }
it { expect(subdir_file.id).to eq('7e3e39ebb9b2bf433b4ad17313770fbe4051649c') }
it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(subdir_file.name).to eq('popen.rb') }
it { expect(subdir_file.path).to eq('files/ruby/popen.rb') }
end
end
describe :file do
let(:file) { tree.select(&:file?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') }
it { expect(file.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(file.name).to eq('.gitignore') }
end
describe :readme do
let(:file) { tree.select(&:readme?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('README.md') }
end
describe :contributing do
let(:file) { tree.select(&:contributing?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('CONTRIBUTING.md') }
end
describe :submodule do
let(:submodule) { tree.select(&:submodule?).first }
it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') }
it { expect(submodule.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
it { expect(submodule.name).to eq('gitlab-shell') }
end
end
end
require 'spec_helper'
describe Gitlab::Git::Util do
describe :count_lines do
[
["", 0],
["foo", 1],
["foo\n", 1],
["foo\n\n", 2],
].each do |string, line_count|
it "counts #{line_count} lines in #{string.inspect}" do
expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count)
end
end
end
end
......@@ -146,6 +146,16 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
context 'with project path in URL' do
let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
it 'a new file in project repo' do
post api(url, user), valid_c_params
expect(response).to have_http_status(201)
end
end
end
context :delete do
......
......@@ -23,6 +23,7 @@ describe API::Groups, api: true do
context "when unauthenticated" do
it "returns authentication error" do
get api("/groups")
expect(response).to have_http_status(401)
end
end
......@@ -30,6 +31,7 @@ describe API::Groups, api: true do
context "when authenticated as user" do
it "normal user: returns an array of groups of user1" do
get api("/groups", user1)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
......@@ -48,6 +50,7 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "admin: returns an array of all groups" do
get api("/groups", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
......@@ -94,6 +97,7 @@ describe API::Groups, api: true do
it "returns all groups you have access to" do
public_group = create :group, :public
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
......@@ -140,6 +144,7 @@ describe API::Groups, api: true do
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/groups/owned')
expect(response).to have_http_status(401)
end
end
......@@ -147,6 +152,7 @@ describe API::Groups, api: true do
context 'when authenticated as group owner' do
it 'returns an array of groups the user owns' do
get api('/groups/owned', user2)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(group2.name)
......@@ -179,6 +185,7 @@ describe API::Groups, api: true do
it "does not return a non existing group" do
get api("/groups/1328", user1)
expect(response).to have_http_status(404)
end
......@@ -192,12 +199,14 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "returns any existing group" do
get api("/groups/#{group2.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group2.name)
end
it "does not return a non existing group" do
get api("/groups/1328", admin)
expect(response).to have_http_status(404)
end
end
......@@ -205,12 +214,14 @@ describe API::Groups, api: true do
context 'when using group path in URL' do
it 'returns any existing group' do
get api("/groups/#{group1.path}", admin)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group1.name)
end
it 'does not return a non existing group' do
get api('/groups/unknown', admin)
expect(response).to have_http_status(404)
end
......@@ -302,6 +313,7 @@ describe API::Groups, api: true do
it "does not return a non existing group" do
get api("/groups/1328/projects", user1)
expect(response).to have_http_status(404)
end
......@@ -325,6 +337,7 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "should return any existing group" do
get api("/groups/#{group2.id}/projects", admin)
expect(response).to have_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
......@@ -332,6 +345,7 @@ describe API::Groups, api: true do
it "should not return a non existing group" do
get api("/groups/1328/projects", admin)
expect(response).to have_http_status(404)
end
end
......@@ -347,6 +361,7 @@ describe API::Groups, api: true do
it 'does not return a non existing group' do
get api('/groups/unknown/projects', admin)
expect(response).to have_http_status(404)
end
......@@ -362,6 +377,7 @@ describe API::Groups, api: true do
context "when authenticated as user without group permissions" do
it "does not create group" do
post api("/groups", user1), attributes_for(:group)
expect(response).to have_http_status(403)
end
end
......@@ -371,6 +387,7 @@ describe API::Groups, api: true do
group = attributes_for(:group, { request_access_enabled: false })
post api("/groups", user3), group
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(group[:name])
......@@ -380,17 +397,20 @@ describe API::Groups, api: true do
it "does not create group, duplicate" do
post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
expect(response).to have_http_status(400)
expect(response.message).to eq("Bad Request")
end
it "returns 400 bad request error if name not given" do
post api("/groups", user3), { path: group2.path }
expect(response).to have_http_status(400)
end
it "returns 400 bad request error if path not given" do
post api("/groups", user3), { name: 'test' }
expect(response).to have_http_status(400)
end
end
......@@ -400,18 +420,22 @@ describe API::Groups, api: true do
context "when authenticated as user" do
it "removes group" do
delete api("/groups/#{group1.id}", user1)
expect(response).to have_http_status(200)
end
it "does not remove a group if not an owner" do
user4 = create(:user)
group1.add_master(user4)
delete api("/groups/#{group1.id}", user3)
expect(response).to have_http_status(403)
end
it "does not remove a non existing group" do
delete api("/groups/1328", user1)
expect(response).to have_http_status(404)
end
......@@ -425,11 +449,13 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
expect(response).to have_http_status(200)
end
it "does not remove a non existing group" do
delete api("/groups/1328", admin)
expect(response).to have_http_status(404)
end
end
......@@ -437,15 +463,17 @@ describe API::Groups, api: true do
describe "POST /groups/:id/projects/:project_id" do
let(:project) { create(:project) }
let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
before(:each) do
allow_any_instance_of(Projects::TransferService).
to receive(:execute).and_return(true)
allow(Project).to receive(:find).and_return(project)
end
context "when authenticated as user" do
it "does not transfer project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", user2)
expect(response).to have_http_status(403)
end
end
......@@ -453,8 +481,45 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
expect(response).to have_http_status(201)
end
context 'when using project path in URL' do
context 'with a valid project path' do
it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project_path}", admin)
expect(response).to have_http_status(201)
end
end
context 'with a non-existent project path' do
it "does not transfer project to group" do
post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
expect(response).to have_http_status(404)
end
end
end
context 'when using a group path in URL' do
context 'with a valid group path' do
it "transfers project to group" do
post api("/groups/#{group1.path}/projects/#{project_path}", admin)
expect(response).to have_http_status(201)
end
end
context 'with a non-existent group path' do
it "does not transfer project to group" do
post api("/groups/noexist/projects/#{project_path}", admin)
expect(response).to have_http_status(404)
end
end
end
end
end
end
......@@ -6,7 +6,7 @@ describe API::Services, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
let(:project) {create(:empty_project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do
......@@ -92,6 +92,8 @@ describe API::Services, api: true do
describe 'POST /projects/:id/services/:slug/trigger' do
let!(:project) { create(:empty_project) }
describe 'Mattermost Service' do
let(:service_name) { 'mattermost_slash_commands' }
context 'no service is available' do
......@@ -107,30 +109,30 @@ describe API::Services, api: true do
let(:params) { { token: 'token' } }
context 'the service is not active' do
let!(:inactive_service) do
before do
project.create_mattermost_slash_commands_service(
active: false,
properties: { token: 'token' }
properties: params
)
end
it 'when the service is inactive' do
post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
post api("/projects/#{project.id}/services/#{service_name}/trigger"), params
expect(response).to have_http_status(404)
end
end
context 'the service is active' do
let!(:active_service) do
before do
project.create_mattermost_slash_commands_service(
active: true,
properties: { token: 'token' }
properties: params
)
end
it 'returns status 200' do
post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
post api("/projects/#{project.id}/services/#{service_name}/trigger"), params
expect(response).to have_http_status(200)
end
......@@ -138,7 +140,7 @@ describe API::Services, api: true do
context 'when the project can not be found' do
it 'returns a generic 404' do
post api("/projects/404/services/mattermost_slash_commands/trigger"), params
post api("/projects/404/services/#{service_name}/trigger"), params
expect(response).to have_http_status(404)
expect(json_response["message"]).to eq("404 Service Not Found")
......@@ -146,4 +148,23 @@ describe API::Services, api: true do
end
end
end
describe 'Slack Service' do
let(:service_name) { 'slack_slash_commands' }
before do
project.create_slack_slash_commands_service(
active: true,
properties: { token: 'token' }
)
end
it 'returns status 200' do
post api("/projects/#{project.id}/services/#{service_name}/trigger"), token: 'token', text: 'help'
expect(response).to have_http_status(200)
expect(json_response['response_type']).to eq("ephemeral")
end
end
end
end
RSpec::Matchers.define :be_valid_commit do
match do |actual|
actual &&
actual.id == SeedRepo::Commit::ID &&
actual.message == SeedRepo::Commit::MESSAGE &&
actual.author_name == SeedRepo::Commit::AUTHOR_FULL_NAME
end
end
# This file is specific to specs in spec/lib/gitlab/git/
SEED_REPOSITORY_PATH = File.expand_path('../../tmp/repositories', __dir__)
TEST_REPO_PATH = File.join(SEED_REPOSITORY_PATH, 'gitlab-git-test.git')
TEST_NORMAL_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "not-bare-repo.git")
TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
module SeedHelper
GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git"
def ensure_seeds
if File.exist?(SEED_REPOSITORY_PATH)
FileUtils.rm_r(SEED_REPOSITORY_PATH)
end
FileUtils.mkdir_p(SEED_REPOSITORY_PATH)
create_bare_seeds
create_normal_seeds
create_mutable_seeds
create_broken_seeds
create_git_attributes
create_invalid_git_attributes
end
def create_bare_seeds
system(git_env, *%W(git clone --bare #{GITLAB_URL}),
chdir: SEED_REPOSITORY_PATH,
out: '/dev/null',
err: '/dev/null')
end
def create_normal_seeds
system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
out: '/dev/null',
err: '/dev/null')
end
def create_mutable_seeds
system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
out: '/dev/null',
err: '/dev/null')
system(git_env, *%w(git branch -t feature origin/feature),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
system(git_env, *%W(git remote add expendable #{GITLAB_URL}),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
end
def create_broken_seeds
system(git_env, *%W(git clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
out: '/dev/null',
err: '/dev/null')
refs_path = File.join(TEST_BROKEN_REPO_PATH, 'refs')
FileUtils.rm_r(refs_path)
end
def create_git_attributes
dir = File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git', 'info')
FileUtils.mkdir_p(dir)
File.open(File.join(dir, 'attributes'), 'w') do |handle|
handle.write <<-EOF.strip
# This is a comment, it should be ignored.
*.txt text
*.jpg -text
*.sh eol=lf gitlab-language=shell
*.haml.* gitlab-language=haml
foo/bar.* foo
*.cgi key=value?p1=v1&p2=v2
/*.png gitlab-language=png
*.binary binary
# This uses a tab instead of spaces to ensure the parser also supports this.
*.md\tgitlab-language=markdown
bla/bla.txt
EOF
end
end
def create_invalid_git_attributes
dir = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git', 'info')
FileUtils.mkdir_p(dir)
enc = Encoding::UTF_16
File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
handle.write('# hello'.encode(enc))
end
end
# Prevent developer git configurations from being persisted to test
# repositories
def git_env
{ 'GIT_TEMPLATE_DIR' => '' }
end
end
RSpec.configure do |config|
config.include SeedHelper, :seed_helper
config.before(:all, :seed_helper) do
ensure_seeds
end
end
# Seed repo:
# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
# b0e52af38d7ea43cf41d8a6f2471351ac036d6c9 Empty commit
# 40f4a7a617393735a95a0bb67b08385bc1e7c66d Add ISO-8859-encoded file
# 66028349a123e695b589e09a36634d976edcc5e8 Merge branch 'add-comments-to-gitmodules' into 'master'
# de5714f34c4e34f1d50b9a61a2e6c9132fe2b5fd Add comments to the end of .gitmodules to test parsing
# fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 Merge branch 'add-files' into 'master'
# eb49186cfa5c4338011f5f590fac11bd66c5c631 Add submodules nested deeper than the root
# 18d9c205d0d22fdf62bc2f899443b83aafbf941f Add executables and links files
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
# 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files
# 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules
# d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files
# c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files
# ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
# 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified
# 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image
# 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
# 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more
# cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule
# 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide
# 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit
module SeedRepo
module BigCommit
ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
MESSAGE = "Files, encoding and much more"
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
FILES_COUNT = 2
end
module Commit
ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"]
FILES_COUNT = 2
C_FILE_PATH = "files/ruby"
C_FILES = ["popen.rb", "regex.rb", "version_info.rb"]
BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
BLOB_FILE_PATH = "app/views/keys/show.html.haml"
end
module EmptyCommit
ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
MESSAGE = "Empty commit"
AUTHOR_FULL_NAME = "Rémy Coutable"
FILES = []
FILES_COUNT = FILES.count
end
module EncodingCommit
ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8"
MESSAGE = "Add ISO-8859-encoded file"
AUTHOR_FULL_NAME = "Stan Hu"
FILES = ["encoding/iso8859.txt"]
FILES_COUNT = FILES.count
end
module FirstCommit
ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
PARENT_ID = nil
MESSAGE = "Initial commit"
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
FILES = ["LICENSE", ".gitignore", "README.md"]
FILES_COUNT = 3
end
module LastCommit
ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6"
PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9"
MESSAGE = "Merge branch 'master' into 'master'"
AUTHOR_FULL_NAME = "Stan Hu"
FILES = ["bin/executable"]
FILES_COUNT = FILES.count
end
module Repo
HEAD = "master"
BRANCHES = %w[
feature
fix
fix-blob-path
fix-existing-submodule-dir
fix-mode
gitattributes
gitattributes-updated
master
merge-test
]
TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
end
module RubyBlob
ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
NAME = "popen.rb"
CONTENT = <<-eos
require 'fileutils'
require 'open3'
module Popen
extend self
def popen(cmd, path=nil)
unless cmd.is_a?(Array)
raise RuntimeError, "System commands must be given as an array of strings"
end
path ||= Dir.pwd
vars = {
"PWD" => path
}
options = {
chdir: path
}
unless File.directory?(path)
FileUtils.mkdir_p(path)
end
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
end
return @cmd_output, @cmd_status
end
end
eos
end
end
......@@ -29,7 +29,7 @@ describe 'projects/pipelines/show' do
render
expect(rendered).to have_css('.js-pipeline-graph')
expect(rendered).to have_css('.grouped-pipeline-dropdown')
expect(rendered).to have_css('.js-grouped-pipeline-dropdown')
# stages
expect(rendered).to have_text('Build')
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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