summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG12
-rw-r--r--app/assets/images/koding-logo.svg8
-rw-r--r--app/assets/javascripts/gl_dropdown.js74
-rw-r--r--app/assets/javascripts/search_autocomplete.js18
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb1
-rw-r--r--app/controllers/koding_controller.rb15
-rw-r--r--app/finders/todos_finder.rb6
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb54
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/concerns/issuable.rb19
-rw-r--r--app/models/concerns/sortable.rb14
-rw-r--r--app/models/repository.rb10
-rw-r--r--app/models/todo.rb19
-rw-r--r--app/views/admin/application_settings/_form.html.haml19
-rw-r--r--app/views/dashboard/todos/index.html.haml19
-rw-r--r--app/views/koding/index.html.haml9
-rw-r--r--app/views/layouts/koding.html.haml5
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml5
-rw-r--r--app/views/projects/buttons/_koding.html.haml7
-rw-r--r--app/views/projects/merge_requests/_show.html.haml3
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--config/routes.rb5
-rw-r--r--db/migrate/20160817133006_add_koding_to_application_settings.rb10
-rw-r--r--db/schema.rb2
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/img/koding_build-in-progress.pngbin0 -> 70949 bytes
-rw-r--r--doc/integration/img/koding_build-logs.pngbin0 -> 263623 bytes
-rw-r--r--doc/integration/img/koding_build-success.pngbin0 -> 304666 bytes
-rw-r--r--doc/integration/img/koding_commit-koding.yml.pngbin0 -> 302703 bytes
-rw-r--r--doc/integration/img/koding_different-stack-on-mr-try.pngbin0 -> 333649 bytes
-rw-r--r--doc/integration/img/koding_edit-on-ide.pngbin0 -> 330880 bytes
-rw-r--r--doc/integration/img/koding_enable-koding.pngbin0 -> 73499 bytes
-rw-r--r--doc/integration/img/koding_landing.pngbin0 -> 268455 bytes
-rw-r--r--doc/integration/img/koding_open-gitlab-from-koding.pngbin0 -> 32559 bytes
-rw-r--r--doc/integration/img/koding_run-in-ide.pngbin0 -> 65465 bytes
-rw-r--r--doc/integration/img/koding_run-mr-in-ide.pngbin0 -> 339759 bytes
-rw-r--r--doc/integration/img/koding_set-up-ide.pngbin0 -> 207481 bytes
-rw-r--r--doc/integration/img/koding_stack-import.pngbin0 -> 500352 bytes
-rw-r--r--doc/integration/img/koding_start-build.pngbin0 -> 105253 bytes
-rw-r--r--doc/integration/koding-usage.md122
-rw-r--r--doc/integration/koding.md239
-rw-r--r--lib/gitlab/current_settings.rb1
-rw-r--r--spec/features/security/dashboard_access_spec.rb14
-rw-r--r--spec/features/todos/todos_sorting_spec.rb67
-rw-r--r--spec/finders/todos_finder_spec.rb70
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml16
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6119
-rw-r--r--spec/javascripts/search_autocomplete_spec.js8
-rw-r--r--spec/routing/routing_spec.rb9
51 files changed, 958 insertions, 58 deletions
diff --git a/CHANGELOG b/CHANGELOG
index f8c82e1ba97..c8c3727873f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@ v 8.11.0 (unreleased)
- Use test coverage value from the latest successful pipeline in badge. !5862
- Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
+ - Add Koding (online IDE) integration
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
- Add delimiter to project stars and forks count (ClemMakesApps)
@@ -44,6 +45,7 @@ v 8.11.0 (unreleased)
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
- Fix awardable button mutuality loading spinners (ClemMakesApps)
+ - Sort todos by date and priority
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes
- Send notification emails to users newly mentioned in issue and MR edits !5800
@@ -163,6 +165,10 @@ v 8.11.0 (unreleased)
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
+v 8.10.7
+ - Upgrade Hamlit to 2.6.1. !5873
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Restore "Largest repository" sort option on Admin > Projects page. !5797
@@ -386,6 +392,9 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
+v 8.9.8
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.9.7
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Require administrator privileges to perform a project import.
@@ -655,6 +664,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
+v 8.8.9
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.8.8
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg
new file mode 100644
index 00000000000..ad89d684d94
--- /dev/null
+++ b/app/assets/images/koding-logo.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
+ <g fill="#d6d7d9">
+ <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
+ <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
+ <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d3394fae3f9..f85aa6dd7b7 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -31,9 +31,8 @@
this.input
.on('keydown', function (e) {
var keyCode = e.which;
-
if (keyCode === 13) {
- e.preventDefault()
+ e.preventDefault();
}
})
.on('keyup', function(e) {
@@ -111,9 +110,9 @@
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
- return $el.show();
+ return $el.show().removeClass('option-hidden');
} else {
- return $el.hide();
+ return $el.hide().addClass('option-hidden');
}
}
});
@@ -179,7 +178,7 @@
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
LOADING_CLASS = "is-loading";
@@ -191,6 +190,12 @@
currentIndex = -1;
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
+
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5
+
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
@@ -213,6 +218,7 @@
if (this.options.data) {
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
+ currentIndex = -1;
this.parseData(this.options.data);
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
@@ -240,7 +246,7 @@
keys: searchFields,
elements: (function(_this) {
return function() {
- selector = '.dropdown-content li:not(.divider)';
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -256,7 +262,7 @@
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
- selector = '.dropdown-content li:not(.divider):visible';
+ selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -376,7 +382,7 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
- if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
@@ -387,7 +393,7 @@
GitLabDropdown.prototype.opened = function() {
var contentHtml;
- currentIndex = -1;
+ this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
@@ -410,6 +416,7 @@
GitLabDropdown.prototype.hidden = function(e) {
var $input;
+ this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
@@ -463,7 +470,7 @@
return "<li class='separator'></li>";
}
if (data.header != null) {
- return "<li class='dropdown-header'>" + data.header + "</li>";
+ return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
html = this.options.renderRow.call(this.options, data, this);
@@ -494,11 +501,16 @@
text = this.highlightTextMatches(text, this.filterInput.val());
}
if (group) {
- groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
+ groupAttrs = 'data-group=' + group + ' data-index=' + index;
} else {
groupAttrs = '';
}
- html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
+ html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
+ url: url,
+ groupAttrs: groupAttrs,
+ cssClass: cssClass,
+ text: text
+ });
}
return html;
};
@@ -520,17 +532,6 @@
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
- GitLabDropdown.prototype.highlightRow = function(index) {
- var selector;
- if (this.filterInput.val() !== "") {
- selector = '.dropdown-content li:first-child a';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content li:first-child a";
- }
- return this.getElement(selector).addClass('is-focused');
- }
- };
-
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
fieldName = this.options.fieldName;
@@ -609,13 +610,15 @@
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
- selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
$el = $(selector, this.dropdown);
if ($el.length) {
- return $el.first().trigger('click');
+ $el.first().trigger('click');
+ var href = $el.attr('href');
+ if (href && href !== '#') Turbolinks.visit(href);
}
};
@@ -623,7 +626,7 @@
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
- selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
+ selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
@@ -651,7 +654,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
- return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
+ return _this.selectRowAtIndex(currentIndex);
}
};
})(this));
@@ -661,6 +664,11 @@
return $('body').off('keydown');
};
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
+
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
$('.is-focused', this.dropdown).removeClass('is-focused');
@@ -674,10 +682,14 @@
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
- if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
- } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
+ if (!index) {
+ $dropdownContent.scrollTop(0)
+ } else if (index === ($listItems.length - 1)) {
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990f6536eb2..227e8c696b4 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -7,7 +7,9 @@
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
- ENTER: 13
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
};
function SearchAutocomplete(opts) {
@@ -223,6 +225,12 @@
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
default:
if (this.searchInput.val() === '') {
this.disableAutocomplete();
@@ -319,9 +327,11 @@
};
SearchAutocomplete.prototype.disableAutocomplete = function() {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open');
- return this.restoreMenu();
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
+ }
};
SearchAutocomplete.prototype.restoreMenu = function() {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9e1dc15de84..6ef7cf0bae6 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,6 +109,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
+ :koding_enabled,
+ :koding_url,
:email_author_in_body,
:repository_checks_enabled,
:metrics_packet_size,
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index c8390af3b36..d425d0f9014 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
def index
+ @sort = params[:sort]
@todos = @todos.page(params[:page])
end
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
new file mode 100644
index 00000000000..bb89f3090f9
--- /dev/null
+++ b/app/controllers/koding_controller.rb
@@ -0,0 +1,15 @@
+class KodingController < ApplicationController
+ before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ layout 'koding'
+
+ def index
+ path = File.join(Rails.root, 'doc/integration/koding-usage.md')
+ @markdown = File.read(path)
+ end
+
+ private
+
+ def check_integration!
+ render_404 unless current_application_settings.koding_enabled?
+ end
+end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 37bad596a16..06b3e8a9502 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -33,7 +33,7 @@ class TodosFinder
# the project IDs yielded by the todos query thus far
items = by_project(items)
- items.reorder(id: :desc)
+ sort(items)
end
private
@@ -106,6 +106,10 @@ class TodosFinder
params[:type]
end
+ def sort(items)
+ params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ end
+
def by_action(items)
if action?
items = items.where(action: to_action_id)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 78c0b79d2bd..6de25bea654 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -31,6 +31,10 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
+ def koding_enabled?
+ current_application_settings.koding_enabled?
+ end
+
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 505545fbabb..249d18c4486 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -236,6 +236,60 @@ module ProjectsHelper
)
end
+ def add_koding_stack_path(project)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch || 'master',
+ file_name: '.koding.yml',
+ commit_message: "Add Koding stack script",
+ content: <<-CONTENT.strip_heredoc
+ provider:
+ aws:
+ access_key: '${var.aws_access_key}'
+ secret_key: '${var.aws_secret_key}'
+ resource:
+ aws_instance:
+ #{project.path}-vm:
+ instance_type: t2.nano
+ user_data: |-
+
+ # Created by GitLab UI for :>
+
+ echo _KD_NOTIFY_@Installing Base packages...@
+
+ apt-get update -y
+ apt-get install git -y
+
+ echo _KD_NOTIFY_@Cloning #{project.name}...@
+
+ export KODING_USER=${var.koding_user_username}
+ export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
+ export BRANCH=${var.koding_queryString_branch}
+
+ sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
+
+ echo _KD_NOTIFY_@#{project.name} cloned.@
+ CONTENT
+ )
+ end
+
+ def koding_project_url(project = nil, branch = nil, sha = nil)
+ if project
+ import_path = "/Home/Stacks/import"
+
+ repo = project.path_with_namespace
+ branch ||= project.default_branch
+ sha ||= project.commit.short_id
+
+ path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
+
+ return URI.join(current_application_settings.koding_url, path).to_s
+ end
+
+ current_application_settings.koding_url
+ end
+
def contribution_guide_path(project)
if project && contribution_guide = project.repository.contribution_guide
namespace_project_blob_path(
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8c19d9dc9c8..f0bcb2d7cda 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -55,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :akismet_enabled
+ validates :koding_url,
+ presence: true,
+ if: :koding_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -149,6 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
+ koding_enabled: false,
+ koding_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index cbae1cd439b..afb5ce37c06 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -131,7 +131,10 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
- select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+ condition_field = "#{table_name}.id"
+ highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
@@ -159,20 +162,6 @@ module Issuable
grouping_columns
end
-
- private
-
- def highest_label_priority(excluded_labels)
- query = Label.select(Label.arel_table[:priority].minimum).
- joins(:label_links).
- where(label_links: { target_type: name }).
- where("label_links.target_id = #{table_name}.id").
- reorder(nil)
-
- query.where.not(title: excluded_labels) if excluded_labels.present?
-
- query
- end
end
def today?
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd..1ebecd86af9 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -35,5 +35,19 @@ module Sortable
all
end
end
+
+ private
+
+ def highest_label_priority(object_types, condition_field, excluded_labels: [])
+ query = Label.select(Label.arel_table[:priority].minimum).
+ joins(:label_links).
+ where(label_links: { target_type: object_types }).
+ where("label_links.target_id = #{condition_field}").
+ reorder(nil)
+
+ query.where.not(title: excluded_labels) if excluded_labels.present?
+
+ query
+ end
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2494c266cd2..bdc3b9d1c1c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -277,7 +277,7 @@ class Repository
def cache_keys
%i(size commit_count
readme version contribution_guide changelog
- license_blob license_key gitignore)
+ license_blob license_key gitignore koding_yml)
end
# Keys for data on branch/tag operations.
@@ -553,6 +553,14 @@ class Repository
end
end
+ def koding_yml
+ return nil unless head_exists?
+
+ cache.fetch(:koding_yml) do
+ file_on_head(/\A\.koding\.yml\z/)
+ end
+ end
+
def gitlab_ci_yml
return nil unless head_exists?
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8d7a5965aa1..6ae9956ade5 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,4 +1,6 @@
class Todo < ActiveRecord::Base
+ include Sortable
+
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
@@ -41,6 +43,23 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
+ class << self
+ def sort(method)
+ method == "priority" ? order_by_labels_priority : order_by(method)
+ end
+
+ # Order by priority depending on which issue/merge request the Todo belongs to
+ # Todos with highest priority first then oldest todos
+ # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
+ def order_by_labels_priority
+ highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+ order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
+ order('todos.created_at')
+ end
+ end
+
def build_failed?
action == BUILD_FAILED
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index c7fd344eea2..e0878512e62 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -388,6 +388,25 @@
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ %fieldset
+ %legend Koding
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding integration documentation", help_page_path("integration/koding")
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4e340b6ec16..d320d3bcc1e 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -43,6 +43,25 @@
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Action'})
+ .pull-right
+ .dropdown.inline.prepend-left-10
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to todos_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to todos_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+
+
.prepend-top-default
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
new file mode 100644
index 00000000000..111cc67336c
--- /dev/null
+++ b/app/views/koding/index.html.haml
@@ -0,0 +1,9 @@
+.row-content-block.second-block.center
+ %p
+ = icon('circle', class: 'cgreen')
+ Integration is active for
+ = link_to koding_project_url, target: '_blank' do
+ #{current_application_settings.koding_url}
+
+.documentation.wiki
+ = markdown @markdown
diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml
new file mode 100644
index 00000000000..22319bba745
--- /dev/null
+++ b/app/views/layouts/koding.html.haml
@@ -0,0 +1,5 @@
+- page_title "Koding"
+- page_description "Koding Dashboard"
+- header_title "Koding", koding_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 3a14751ea8e..67f558c854b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -12,6 +12,11 @@
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
%span
Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
+ %span
+ Koding
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
%span
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
new file mode 100644
index 00000000000..fdc80d44253
--- /dev/null
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -0,0 +1,7 @@
+- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
+ - if @repository.koding_yml
+ = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
+ Run in IDE (Koding)
+ - else
+ = link_to add_koding_stack_path(@project), class: 'btn' do
+ Set Up Koding
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index f8025fc1dbe..9d8b4cc56be 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -16,6 +16,9 @@
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
+ - if koding_enabled? && @repository.koding_yml
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index a666d07e9eb..340e159c874 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -64,10 +64,12 @@
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
+
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
+ = render "projects/buttons/koding"
.btn-group.project-repo-btn-group
= render "projects/buttons/download"
@@ -86,4 +88,4 @@
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
- = render default_project_view \ No newline at end of file
+ = render default_project_view
diff --git a/config/routes.rb b/config/routes.rb
index 66f77aee06a..e93b640fbc0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -91,6 +91,11 @@ Rails.application.routes.draw do
get 'help/*path' => 'help#show', as: :help_page
#
+ # Koding route
+ #
+ get 'koding' => 'koding#index'
+
+ #
# Global snippets
#
resources :snippets do
diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb
new file mode 100644
index 00000000000..915d3d78e40
--- /dev/null
+++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb
@@ -0,0 +1,10 @@
+class AddKodingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :koding_enabled, :boolean
+ add_column :application_settings, :koding_url, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a94d0cc97af..6cbc766831b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -90,6 +90,8 @@ ActiveRecord::Schema.define(version: 20160819221833) do
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
+ t.boolean "koding_enabled"
+ t.string "koding_url"
end
create_table "audit_events", force: :cascade do |t|
diff --git a/doc/integration/README.md b/doc/integration/README.md
index ddbd570ac6c..70895abbcad 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services.
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
+- [Koding](koding.md) Configure Koding to use IDE integration
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
diff --git a/doc/integration/img/koding_build-in-progress.png b/doc/integration/img/koding_build-in-progress.png
new file mode 100644
index 00000000000..f8cc81834c4
--- /dev/null
+++ b/doc/integration/img/koding_build-in-progress.png
Binary files differ
diff --git a/doc/integration/img/koding_build-logs.png b/doc/integration/img/koding_build-logs.png
new file mode 100644
index 00000000000..a04cd5aff99
--- /dev/null
+++ b/doc/integration/img/koding_build-logs.png
Binary files differ
diff --git a/doc/integration/img/koding_build-success.png b/doc/integration/img/koding_build-success.png
new file mode 100644
index 00000000000..2a0dd296480
--- /dev/null
+++ b/doc/integration/img/koding_build-success.png
Binary files differ
diff --git a/doc/integration/img/koding_commit-koding.yml.png b/doc/integration/img/koding_commit-koding.yml.png
new file mode 100644
index 00000000000..3e133c50327
--- /dev/null
+++ b/doc/integration/img/koding_commit-koding.yml.png
Binary files differ
diff --git a/doc/integration/img/koding_different-stack-on-mr-try.png b/doc/integration/img/koding_different-stack-on-mr-try.png
new file mode 100644
index 00000000000..fd25e32f648
--- /dev/null
+++ b/doc/integration/img/koding_different-stack-on-mr-try.png
Binary files differ
diff --git a/doc/integration/img/koding_edit-on-ide.png b/doc/integration/img/koding_edit-on-ide.png
new file mode 100644
index 00000000000..fd5aaff75f5
--- /dev/null
+++ b/doc/integration/img/koding_edit-on-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_enable-koding.png b/doc/integration/img/koding_enable-koding.png
new file mode 100644
index 00000000000..c0ae0ee9918
--- /dev/null
+++ b/doc/integration/img/koding_enable-koding.png
Binary files differ
diff --git a/doc/integration/img/koding_landing.png b/doc/integration/img/koding_landing.png
new file mode 100644
index 00000000000..7c629d9b05e
--- /dev/null
+++ b/doc/integration/img/koding_landing.png
Binary files differ
diff --git a/doc/integration/img/koding_open-gitlab-from-koding.png b/doc/integration/img/koding_open-gitlab-from-koding.png
new file mode 100644
index 00000000000..c958cf8f224
--- /dev/null
+++ b/doc/integration/img/koding_open-gitlab-from-koding.png
Binary files differ
diff --git a/doc/integration/img/koding_run-in-ide.png b/doc/integration/img/koding_run-in-ide.png
new file mode 100644
index 00000000000..f91ee0f74cc
--- /dev/null
+++ b/doc/integration/img/koding_run-in-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_run-mr-in-ide.png b/doc/integration/img/koding_run-mr-in-ide.png
new file mode 100644
index 00000000000..502817a2a46
--- /dev/null
+++ b/doc/integration/img/koding_run-mr-in-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_set-up-ide.png b/doc/integration/img/koding_set-up-ide.png
new file mode 100644
index 00000000000..7f408c980b5
--- /dev/null
+++ b/doc/integration/img/koding_set-up-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_stack-import.png b/doc/integration/img/koding_stack-import.png
new file mode 100644
index 00000000000..2a4e3c87fc8
--- /dev/null
+++ b/doc/integration/img/koding_stack-import.png
Binary files differ
diff --git a/doc/integration/img/koding_start-build.png b/doc/integration/img/koding_start-build.png
new file mode 100644
index 00000000000..52159440f62
--- /dev/null
+++ b/doc/integration/img/koding_start-build.png
Binary files differ
diff --git a/doc/integration/koding-usage.md b/doc/integration/koding-usage.md
new file mode 100644
index 00000000000..bb74badce66
--- /dev/null
+++ b/doc/integration/koding-usage.md
@@ -0,0 +1,122 @@
+# Koding & GitLab
+
+This document will guide you through using Koding integration on GitLab in
+detail. For configuring and installing please follow [this](koding.md) guide.
+
+You can use Koding integration to run and develop your projects on GitLab. This
+will allow you and the users to test your project without leaving the browser.
+Koding handles projects as stacks which are basic recipes to define your
+environment for your project. With this integration you can automatically
+create a proper stack template for your projects. Currently auto-generated
+stack templates are designed to work with AWS which requires a valid AWS
+credential to be able to use these stacks. You can find more information about
+stacks and the other providers that you can use on Koding from
+[here](https://www.koding.com/docs).
+
+
+# Enable Integration
+
+You can enable Koding integration by providing the running Koding instance URL
+in Application Settings;
+
+ - Open **Admin area > Settings** (`/admin/application_settings`).
+
+![Enable Koding](help/integration/img/koding_enable-koding.png)
+
+Once enabled you will see `Koding` link on your sidebar which leads you to
+Koding Landing page
+
+![Koding Landing](help/integration/img/koding_landing.png)
+
+You can navigate to running Koding instance from here. For more information and
+details about configuring integration please follow [this](koding.md) guide.
+
+
+# Set up Koding on Projects
+
+Once it's enabled, you will see some integration buttons on Project pages,
+Merge Requests etc. To get started working on a specific project you first need
+to create a `.koding.yml` file under your project root. You can easily do that
+by using `Set Up Koding` button which will be visible on every project's
+landing page;
+
+![Set Up Koding](help/integration/img/koding_set-up-ide.png)
+
+Once you click this will open a New File page on GitLab with auto-generated
+`.koding.yml` content based on your server and repository configuration.
+
+![Commit .koding.yml](help/integration/img/koding_commit-koding.yml.png)
+
+
+# Run a project on Koding
+
+If there is `.koding.yml` exists in your project root, you will see
+`Run in IDE (Koding)` button in your project landing page. You can initiate the
+process from here.
+
+![Run on Koding](help/integration/img/koding_run-in-ide.png)
+
+This will open Koding defined in the settings in a new window and will start
+importing the project's stack file;
+
+![Import Stack](help/integration/img/koding_stack-import.png)
+
+You should see the details of your repository imported into your Koding
+instance. Once it's completed it will lead you to the Stack Editor and from
+there you can start using your new stack integrated with your project on your
+GitLab instance. For details about what's next you can follow
+[this](https://www.koding.com/docs/creating-an-aws-stack) guide from 8. step.
+
+Once stack initialized you will see the `README.md` content from your project
+in `Stack Build` wizard, this wizard will let you to build the stack and import
+your project into it. **Once it's completed it will automatically open the
+related vm instead of importing from scratch**
+
+![Stack Building](help/integration/img/koding_start-build.png)
+
+This will take time depending on the required environment.
+
+![Stack Building in Progress](help/integration/img/koding_build-in-progress.png)
+
+It usually takes ~4 min. to make it ready with a `t2.nano` instance on given
+AWS region. (`t2.nano` is default vm type on auto-generated stack template
+which can be manually changed)
+
+![Stack Building Success](help/integration/img/koding_build-success.png)
+
+You can check out the `Build Logs` from this success modal as well;
+
+![Stack Build Logs](help/integration/img/koding_build-logs.png)
+
+You can now `Start Coding`!
+
+![Edit On IDE](help/integration/img/koding_edit-on-ide.png)
+
+
+# Try a Merge Request on IDE
+
+It's also possible to try a change on IDE before merging it. This flow only
+enabled if the target project has `.koding.yml` in it's target branch. You
+should see the alternative version of `Run in IDE (Koding)` button in merge
+request pages as well;
+
+![Run in IDE on MR](help/integration/img/koding_run-mr-in-ide.png)
+
+This will again take you to Koding with proper arguments passed, which will
+allow Koding to modify the stack template provided by target branch. You can
+see the difference;
+
+![Different Branch for MR](help/integration/img/koding_different-stack-on-mr-try.png)
+
+The flow for the branch stack is also same with the regular project flow.
+
+
+# Open GitLab from Koding
+
+Since stacks generated with import flow defined in previous steps, they have
+information about the repository they are belonging to. By using this
+information you can access to related GitLab page from stacks on your sidebar
+on Koding.
+
+![Open GitLab from Koding](help/integration/img/koding_open-gitlab-from-koding.png)
+
diff --git a/doc/integration/koding.md b/doc/integration/koding.md
new file mode 100644
index 00000000000..53450b6d048
--- /dev/null
+++ b/doc/integration/koding.md
@@ -0,0 +1,239 @@
+# Koding & GitLab
+
+This document will guide you through installing and configuring Koding with
+GitLab.
+
+First of all, to be able to use Koding and GitLab together you will need public
+access to your server. This allows you to use single sign-on from GitLab to
+Koding and using vms from cloud providers like AWS. Koding has a registry for
+VMs, called Kontrol and it runs on the same server as Koding itself, VMs from
+cloud providers register themselves to Kontrol via the agent that we put into
+provisioned VMs. This agent is called Klient and it provides Koding to access
+and manage the target machine.
+
+Kontrol and Klient are based on another technology called
+[Kite](github.com/koding/kite), that we have written at Koding. Which is a
+microservice framework that allows you to develop microservices easily.
+
+
+## Requirements
+
+### Hardware
+
+Minimum requirements are;
+
+ - 2 cores CPU
+ - 3G RAM
+ - 10G Storage
+
+If you plan to use AWS to install Koding it is recommended that you use at
+least a `c3.xlarge` instance.
+
+### Software
+
+ - [git](https://git-scm.com)
+ - [docker](https://www.docker.com)
+ - [docker-compose](https://www.docker.com/products/docker-compose)
+
+Koding can run on most of the UNIX based operating systems, since it's shipped
+as containerized with Docker support, it can work on any operating system that
+supports Docker.
+
+Required services are;
+
+ - PostgreSQL # Kontrol and Service DB provider
+ - MongoDB # Main DB provider the application
+ - Redis # In memory DB used by both application and services
+ - RabbitMQ # Message Queue for both application and services
+
+which are also provided as a Docker container by Koding.
+
+
+## Getting Started with Development Versions
+
+
+### Koding
+
+You can run `docker-compose` environment for developing koding by
+executing commands in the following snippet.
+
+```bash
+git clone https://github.com/koding/koding.git
+cd koding
+docker-compose up
+```
+
+This should start koding on `localhost:8090`.
+
+By default there is no team exists in Koding DB. You'll need to create a team
+called `gitlab` which is the default team name for GitLab integration in the
+configuration. To make things in order it's recommended to create the `gitlab`
+team first thing after setting up Koding.
+
+
+### GitLab
+
+To install GitLab to your environment for development purposes it's recommended
+to use GitLab Development Kit which you can get it from
+[here](https://gitlab.com/gitlab-org/gitlab-development-kit).
+
+After all those steps, gitlab should be running on `localhost:3000`
+
+
+## Integration
+
+Integration includes following components;
+
+ - Single Sign On with OAuth from GitLab to Koding
+ - System Hook integration for handling GitLab events on Koding
+ (`project_created`, `user_joined` etc.)
+ - Service endpoints for importing/executing stacks from GitLab to Koding
+ (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs)
+
+As it's pointed out before, you will need public access to this machine that
+you've installed Koding and GitLab on. Better to use a domain but a static IP
+is also fine.
+
+For IP based installation you can use [xip.io](https://xip.io) service which is
+free and provides DNS resolution to IP based requests like following;
+
+ - 127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - and so on...
+
+As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
+a running koding instance on `127.0.0.1` server will be handled as `foo` team
+requests.
+
+
+### GitLab Side
+
+You need to enable Koding integration from Settings under Admin Area. To do
+that login with an Admin account and do followings;
+
+ - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings)
+ - scroll to bottom of the page until Koding section
+ - check `Enable Koding` checkbox
+ - provide GitLab team page for running Koding instance as `Koding URL`*
+
+* For `Koding URL` you need to provide the gitlab integration enabled team on
+your Koding installation. Team called `gitlab` has integration on Koding out
+of the box, so if you didn't change anything your team on Koding should be
+`gitlab`.
+
+So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
+to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
+with your Koding installation here.
+
+
+#### Registering Koding for OAuth integration
+
+We need `Application ID` and `Secret` to enable login to Koding via GitLab
+feature and to do that you need to register running Koding as a new application
+to your running GitLab application. Follow
+[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to
+enable this integration.
+
+Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback`
+which again you need to _replace `127.0.0.1` with your instance public IP._
+
+Take a copy of `Application ID` and `Secret` that is generated by the GitLab
+application, we will need those on _Koding Part_ of this guide.
+
+
+#### Registering system hooks to Koding (optional)
+
+Koding can take actions based on the events generated by GitLab application.
+This feature is still in progress and only following events are processed by
+Koding at the moment;
+
+ - user_create
+ - user_destroy
+
+All system events are handled but not implemented on Koding side.
+
+To enable this feature you need to provide a `URL` and a `Secret Token` to your
+GitLab application. Open your admin area on your GitLab app from
+[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks)
+and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the
+endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and
+keep a copy of it, we will need it on _Koding Part_ of this guide.
+
+_(replace `127.0.0.1` with your instance public IP)_
+
+
+### Koding Part
+
+If you followed the steps in GitLab part we should have followings to enable
+Koding part integrations;
+
+ - `Application ID` and `Secret` for OAuth integration
+ - `Secret Token` for system hook integration
+ - Public address of running GitLab instance
+
+
+#### Start Koding with GitLab URL
+
+Now we need to configure Koding with all this information to get things ready.
+If it's already running please stop koding first.
+
+##### From command-line
+
+Replace followings with the ones you got from GitLab part of this guide;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+ --gitlabHost=**GITLAB_IP** \
+ --gitlabPort=**GITLAB_PORT** \
+ --gitlabToken=**SECRET_TOKEN** \
+ --gitlabAppId=**APPLICATION_ID** \
+ --gitlabAppSecret=**SECRET**
+```
+
+##### By updating configuration
+
+Alternatively you can update `gitlab` section on
+`config/credentials.default.coffee` like following;
+
+```
+gitlab =
+ host: '**GITLAB_IP**'
+ port: '**GITLAB_PORT**'
+ applicationId: '**APPLICATION_ID**'
+ applicationSecret: '**SECRET**'
+ team: 'gitlab'
+ redirectUri: ''
+ systemHookToken: '**SECRET_TOKEN**'
+ hooksEnabled: yes
+```
+
+and start by only providing the `host`;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+```
+
+#### Enable Single Sign On
+
+Once you restarted your Koding and logged in with your username and password
+you need to activate oauth authentication for your user. To do that
+
+ - Navigate to Dashboard on Koding from;
+ `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
+ - Scroll down to Integrations section
+ - Click on toggle to turn On integration in GitLab integration section
+
+This will redirect you to your GitLab instance and will ask your permission (
+if you are not logged in to GitLab at this point you will be redirected after
+login) once you accept you will be redirected to your Koding instance.
+
+From now on you can login by using `SIGN IN WITH GITLAB` button on your Login
+screen in your Koding instance.
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 735331df66c..27acd817e51 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -30,6 +30,7 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
+ koding_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 788581a26cb..40f773956d1 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
+ describe "GET /koding" do
+ subject { koding_path }
+
+ context 'with Koding enabled' do
+ before do
+ stub_application_setting(koding_enabled?: true)
+ end
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ end
+ end
+
describe "GET /projects/new" do
it { expect(new_project_path).to be_allowed_for :admin }
it { expect(new_project_path).to be_allowed_for :user }
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 00000000000..e74a51acede
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe "Dashboard > User sorts todos", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
+
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ project.team << [user, :developer]
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
new file mode 100644
index 00000000000..f7e7e733cf7
--- /dev/null
+++ b/spec/finders/todos_finder_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe TodosFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:finder) { described_class }
+
+ before { project.team << [user, :developer] }
+
+ describe '#sort' do
+ context 'by date' do
+ let!(:todo1) { create(:todo, user: user, project: project) }
+ let!(:todo2) { create(:todo, user: user, project: project) }
+ let!(:todo3) { create(:todo, user: user, project: project) }
+
+ it 'sorts with oldest created first' do
+ todos = finder.new(user, { sort: 'id_asc' }).execute
+
+ expect(todos.first).to eq(todo1)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo3)
+ end
+
+ it 'sorts with newest created first' do
+ todos = finder.new(user, { sort: 'id_desc' }).execute
+
+ expect(todos.first).to eq(todo3)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo1)
+ end
+ end
+
+ it "sorts by priority" do
+ label_1 = create(:label, title: 'label_1', project: project, priority: 1)
+ label_2 = create(:label, title: 'label_2', project: project, priority: 2)
+ label_3 = create(:label, title: 'label_3', project: project, priority: 3)
+
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ merge_request_1 = create(:merge_request, source_project: project)
+
+ merge_request_1.labels << label_1
+
+ # Covers the case where Todo has more than one label
+ issue_3.labels << label_1
+ issue_3.labels << label_3
+
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ todo_1 = create(:todo, user: user, project: project, target: issue_4)
+ todo_2 = create(:todo, user: user, project: project, target: issue_2)
+ todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
+ todo_4 = create(:todo, user: user, project: project, target: issue_1)
+ todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ todos = finder.new(user, { sort: 'priority' }).execute
+
+ expect(todos.first).to eq(todo_3)
+ expect(todos.second).to eq(todo_5)
+ expect(todos.third).to eq(todo_4)
+ expect(todos.fourth).to eq(todo_2)
+ expect(todos.fifth).to eq(todo_1)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 00000000000..a20390c08ee
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+ %i.fa.fa-times.dropdown-menu-close-icon
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+ %i.fa.fa-search.dropdown-input-search
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 00000000000..b529ea6458d
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i++;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ fixture.preload('gl_dropdown.html');
+ fixture.preload('projects.json');
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ data: this.projectsData,
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open')
+ let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(Turbolinks, 'visit').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ let linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 68d64483d67..324f5152780 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -105,13 +105,13 @@
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
expect(list.find(a1).length).toBe(1);
- expect(list.find(a1).text()).toBe(' Issues assigned to me ');
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
expect(list.find(a2).length).toBe(1);
- expect(list.find(a2).text()).toBe(" Issues I've created ");
+ expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
- expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
+ expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
- return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
+ return expect(list.find(a4).text()).toBe("Merge requests I've created");
};
describe('Search autocomplete dropdown', function() {
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1d4df9197f6..d65648dd0b2 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -116,12 +116,19 @@ describe HelpController, "routing" do
expect(get(path)).to route_to('help#show',
path: 'workflow/protected_branches/protected_branches1',
format: 'png')
-
+
path = '/help/ui'
expect(get(path)).to route_to('help#ui')
end
end
+# koding GET /koding(.:format) koding#index
+describe KodingController, "routing" do
+ it "to #index" do
+ expect(get("/koding")).to route_to('koding#index')
+ end
+end
+
# profile_account GET /profile/account(.:format) profile#account
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update