diff --git a/CHANGELOG b/CHANGELOG index 5f14e87d66..5f69fb902e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,13 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Fix mass SQL statements on initial push (Hannes Rosenögger) + - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) + - Add comment notification events to HipChat and Slack services (Stan Hu) + - Add issue and merge request events to HipChat and Slack services (Stan Hu) - Fix merge request URL passed to Webhooks. (Stan Hu) - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) + - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. - Improve error messages for file edit failures @@ -13,6 +18,7 @@ v 7.9.0 (unreleased) - Fix ordering of imported but unchanged projects (Marco Wessel) - Mobile UI improvements: make aside content expandable - Expose avatar_url in projects API + - Fix checkbox alignment on the application settings page. - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - Fix mass-unassignment of issues (Robert Speicher) - Allow user confirmation to be skipped for new users via API @@ -20,6 +26,50 @@ v 7.9.0 (unreleased) - Ignore case of LDAP user DN when checking group membership. - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options + - Add grouped milestones from all projects to dashboard. + - Web hook sends pusher email as well as commiter + - Add Bitbucket omniauth provider. + - Add Bitbucket importer. + - Support referencing issues to a project whose name starts with a digit + - Condense commits already in target branch when updating merge request source branch. + - Send notifications and leave system comments when bulk updating issues. + - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) + - Move groups page from profile to dashboard + - Starred projects page at dashboard + - Blocking user does not remove him/her from project/groups but show blocked label + - Change subject of EmailsOnPush emails to include namespace, project and branch. + - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed. + - Remove confusing footer from EmailsOnPush mail body. + - Add list of changed files to EmailsOnPush emails. + - Add option to send EmailsOnPush emails from committer email if domain matches. + - Add option to disable code diffs in EmailOnPush emails. + - Wrap commit message in EmailsOnPush email. + - Send EmailsOnPush emails when deleting commits using force push. + - Fix EmailsOnPush email comparison link to include first commit. + - Fix highliht of selected lines in file + - Reject access to group/project avatar if the user doesn't have access. + - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update) + - Add GitLab active users count to rake gitlab:check + - Starred projects page at dashboard + - Make email display name configurable + - Improve json validation in hook data +v 7.8.4 + - Fix issue_tracker_id substitution in custom issue trackers + - Fix path and name duplication in namespaces + +v 7.8.3 + - Bump version of gitlab_git fixing annotated tags without message + +v 7.8.2 + - Fix service migration issue when upgrading from versions prior to 7.3 + - Fix setting of the default use project limit via admin UI + - Fix showing of already imported projects for GitLab and Gitorious importers + - Fix response of push to repository to return "Not found" if user doesn't have access + - Fix check if user is allowed to view the file attachment + - Fix import check for case sensetive namespaces + - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time. + - Properly handle autosave local storage exceptions. + - Escape wildcards when searching LDAP by username. v 7.8.1 - Fix run of custom post receive hooks @@ -27,9 +77,6 @@ v 7.8.1 - Fix the warning for LDAP users about need to set password - Fix avatars which were not shown for non logged in users - Fix urls for the issues when relative url was enabled - - Add Bitbucket omniauth provider. - - Add Bitbucket importer. - - Support referencing issues to a project whose name starts with a digit v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. @@ -48,7 +95,8 @@ v 7.8.0 - Add notes for label changes in issue and merge requests - Show tags in commit view (Hannes Rosenögger) - Only count a user's vote once on a merge request or issue (Michael Clarke) - - Increate font size when browse source files and diffs + - Increase font size when browse source files and diffs + - Service Templates now let you set default values for all services - Create new file in empty repository using GitLab UI - Ability to clone project using oauth2 token - Upgrade Sidekiq gem to version 3.3.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3d4d8ea9b..42b5ce22e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,13 @@ Please treat our volunteers with courtesy and respect, it will go a long way tow Issues and merge requests should be in English and contain appropriate language for audiences of all ages. +## Helping others + +Please help other GitLab users when you can. +The channnels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/). +Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the irc channel. +You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. + ## Issue tracker To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/). @@ -56,6 +63,8 @@ Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. +To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. + ### Merge request guidelines If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: diff --git a/Gemfile b/Gemfile index 1927fdabec..9eb196e07f 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.0.0.rc14' +gem "gitlab_git", '7.1.0' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' @@ -179,6 +179,9 @@ gem 'ace-rails-ap' # Keyboard shortcuts gem 'mousetrap-rails' +# Shutting down requests that take too long +gem "slowpoke" + gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" @@ -252,7 +255,7 @@ group :development, :test do gem 'jasmine', '2.0.2' - gem "spring", '1.3.1' + gem "spring", '~> 1.3.1' gem "spring-commands-rspec", '1.0.4' gem "spring-commands-spinach", '1.0.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 4157139766..89ef83b01a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,6 +149,7 @@ GEM enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) + errbase (0.0.2) erubis (2.7.0) escape_utils (0.2.4) eventmachine (1.0.4) @@ -212,7 +213,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.0.1.1) emoji (~> 1.0.1) - gitlab_git (7.0.0.rc14) + gitlab_git (7.1.0) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -429,6 +430,7 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) + rack-timeout (0.2.0) rails (4.1.9) actionmailer (= 4.1.9) actionpack (= 4.1.9) @@ -482,6 +484,8 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rinku (1.7.3) + robustly (0.0.3) + errbase rouge (1.7.4) rspec (2.99.0) rspec-core (~> 2.99.0) @@ -568,6 +572,9 @@ GEM temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) slop (3.4.7) + slowpoke (0.0.5) + rack-timeout (>= 0.1.0) + robustly spinach (0.8.7) colorize (= 0.5.8) gherkin-ruby (>= 0.3.1) @@ -706,7 +713,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.0.1.1) - gitlab_git (= 7.0.0.rc14) + gitlab_git (= 7.1.0) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 4.0.0) @@ -779,8 +786,9 @@ DEPENDENCIES six slack-notifier (~> 1.0.0) slim + slowpoke spinach-rails - spring (= 1.3.1) + spring (~> 1.3.1) spring-commands-rspec (= 1.0.4) spring-commands-spinach (= 1.0.0) stamp diff --git a/README.md b/README.md index 91ac311336..852d944360 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,6 @@ To get access to the EE and support please [become a subscriber](https://about.g - [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) -- [![PullReview stats](https://www.pullreview.com/gitlab/gitlab-org/gitlab-ce/badges/master.svg?)](https://www.pullreview.com/gitlab.gitlab.com/gitlab-org/gitlab-ce/reviews/master) - ## Website On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee index 3450f4b55f..5d3fe81da7 100644 --- a/app/assets/javascripts/autosave.js.coffee +++ b/app/assets/javascripts/autosave.js.coffee @@ -14,7 +14,11 @@ class @Autosave restore: -> return unless window.localStorage? - text = window.localStorage.getItem @key + try + text = window.localStorage.getItem @key + catch + return + @field.val text if text?.length > 0 @field.trigger "input" @@ -23,11 +27,13 @@ class @Autosave text = @field.val() if text?.length > 0 - window.localStorage.setItem @key, text + try + window.localStorage.setItem @key, text else @reset() reset: -> return unless window.localStorage? - window.localStorage.removeItem @key \ No newline at end of file + try + window.localStorage.removeItem @key diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee index a5f15f80c5..37a175fdbc 100644 --- a/app/assets/javascripts/blob/blob.js.coffee +++ b/app/assets/javascripts/blob/blob.js.coffee @@ -26,7 +26,7 @@ class @BlobView unless isNaN first_line $("#tree-content-holder .highlight .line").removeClass("hll") $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $.scrollTo("#L#{first_line}") unless e? + $.scrollTo("#L#{first_line}", offset: -50) unless e? # parse selected lines from hash # always return first and last line (initialized to NaN) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 6914ca759f..2e91a06daa 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -15,7 +15,7 @@ class @EditBlob $(".js-commit-button").click -> $("#file-content").val editor.getValue() $(".file-editor form").submit() - return + return false editModePanes = $(".js-edit-mode-pane") editModeLinks = $(".js-edit-mode a") diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee index a6e27116b4..ab8f98715e 100644 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -15,7 +15,7 @@ class @NewBlob $(".js-commit-button").click -> $("#file-content").val editor.getValue() $(".file-editor form").submit() - return + return false editor: -> return @editor diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index 6ef5a539b8..00ee503ff1 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,30 +1,3 @@ class @Dashboard constructor: -> - @initSidebarTab() - - $(".dash-filter").keyup -> - terms = $(this).val() - uiBox = $(this).parents('.panel').first() - if terms == "" || terms == undefined - uiBox.find(".dash-list li").show() - else - uiBox.find(".dash-list li").each (index) -> - name = $(this).find(".filter-title").text() - - if name.toLowerCase().search(terms.toLowerCase()) == -1 - $(this).hide() - else - $(this).show() - - - - initSidebarTab: -> - key = "dashboard_sidebar_filter" - - # store selection in cookie - $('.dash-sidebar-tabs a').on 'click', (e) -> - $.cookie(key, $(e.target).attr('id')) - - # show tab from cookie - sidebar_filter = $.cookie(key) - $("#" + sidebar_filter).tab('show') if sidebar_filter + new ProjectsList() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 0e919aff69..bd61025116 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -33,12 +33,16 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new ZenMode() new DropzoneInput($('.issue-form')) + if page == 'projects:issues:new' + new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' GitLab.GfmAutoComplete.setup() new Diff() shortcut_handler = new ShortcutsNavigation() new ZenMode() new DropzoneInput($('.merge-request-form')) + if page == 'projects:merge_requests:new' + new IssuableForm($('.merge-request-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssueable() @@ -51,6 +55,9 @@ class Dispatcher when 'dashboard:show' new Dashboard() new Activities() + when 'dashboard:projects:starred' + new Activities() + new ProjectsList() when 'projects:commit:show' new Commit() new Diff() @@ -58,9 +65,13 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:commits:show' shortcut_handler = new ShortcutsNavigation() - when 'groups:show', 'projects:show' + when 'projects:show' new Activities() shortcut_handler = new ShortcutsNavigation() + when 'groups:show' + new Activities() + shortcut_handler = new ShortcutsNavigation() + new ProjectsList() when 'groups:members' new GroupMembers() new UsersSelect() diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee new file mode 100644 index 0000000000..abd58bcf97 --- /dev/null +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -0,0 +1,28 @@ +class @IssuableForm + constructor: (@form) -> + @titleField = @form.find("input[name*='[title]']") + @descriptionField = @form.find("textarea[name*='[description]']") + + return unless @titleField.length && @descriptionField.length + + @initAutosave() + + @form.on "submit", @resetAutosave + @form.on "click", ".btn-cancel", @resetAutosave + + initAutosave: -> + new Autosave @titleField, [ + document.location.pathname, + document.location.search, + "title" + ] + + new Autosave @descriptionField, [ + document.location.pathname, + document.location.search, + "description" + ] + + resetAutosave: => + @titleField.data("autosave").reset() + @descriptionField.data("autosave").reset() diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index c42f31933d..d644d50b66 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -49,6 +49,13 @@ class @Milestone data: data success: (data) -> if data.saved == true + if data.assignee_avatar_url + img_tag = $('') + img_tag.attr('src', data.assignee_avatar_url) + img_tag.addClass('avatar s16') + $(li).find('.assignee-icon').html(img_tag) + else + $(li).find('.assignee-icon').html('') $(li).effect 'highlight' else new Flash("Issue update failed", 'alert') diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee index d0eaaad92b..6828ae471e 100644 --- a/app/assets/javascripts/project_show.js.coffee +++ b/app/assets/javascripts/project_show.js.coffee @@ -6,7 +6,7 @@ class @ProjectShow new Flash('Star toggle failed. Try again later.', 'alert') $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> - $.cookie "default_view", $(e.target).attr("href"), { expires: 30 } + $.cookie "default_view", $(e.target).attr("href"), { expires: 30, path: '/' } defaultView = $.cookie("default_view") if defaultView diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee new file mode 100644 index 0000000000..c0e36d1ccc --- /dev/null +++ b/app/assets/javascripts/projects_list.js.coffee @@ -0,0 +1,24 @@ +class @ProjectsList + constructor: -> + $(".projects-list .js-expand").on 'click', (e) -> + e.preventDefault() + list = $(this).closest('.projects-list') + list.find("li").show() + list.find("li.bottom").hide() + + $(".projects-list-filter").keyup -> + terms = $(this).val() + uiBox = $(this).closest('.panel') + if terms == "" || terms == undefined + uiBox.find(".projects-list li").show() + else + uiBox.find(".projects-list li").each (index) -> + name = $(this).find(".filter-title").text() + + if name.toLowerCase().search(terms.toLowerCase()) == -1 + $(this).hide() + else + $(this).show() + uiBox.find(".projects-list li.bottom").hide() + + diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee index 8a2e2421c2..d0d81f9692 100644 --- a/app/assets/javascripts/user.js.coffee +++ b/app/assets/javascripts/user.js.coffee @@ -1,3 +1,4 @@ class @User constructor: -> $('.profile-groups-avatars').tooltip("placement": "top") + new ProjectsList() diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e5bb5e21bb..015ff2ce4e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,12 +11,17 @@ *= require cal-heatmap */ -@import "main/*"; + +@import "base/variables"; +@import "base/mixins"; +@import "base/layout"; + /** * Customized Twitter bootstrap */ -@import 'gl_bootstrap'; +@import 'base/gl_variables'; +@import 'base/gl_bootstrap'; /** * NProgress load bar css @@ -39,7 +44,7 @@ * Page specific styles (issues, projects etc): */ -@import "sections/*"; +@import "pages/*"; /** * Code highlight diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss similarity index 63% rename from app/assets/stylesheets/gl_bootstrap.scss rename to app/assets/stylesheets/base/gl_bootstrap.scss index 6efa56544a..0775c17181 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -3,11 +3,6 @@ * */ -$font-size-base: 13px !default; -$nav-pills-active-link-hover-bg: $bg_primary; -$pagination-active-bg: $bg_primary; -$list-group-active-bg: $bg_primary; - // Core variables and mixins @import "bootstrap/variables"; @import "bootstrap/mixins"; @@ -23,6 +18,7 @@ $list-group-active-bg: $bg_primary; @import "bootstrap/grid"; @import "bootstrap/tables"; @import "bootstrap/forms"; +@import "bootstrap/buttons"; // Components @import "bootstrap/component-animations"; @@ -134,10 +130,6 @@ $list-group-active-bg: $bg_primary; } } } - - &.nav-small-tabs > li > a { - padding: 6px 9px; - } } .nav-tabs > li > a, @@ -145,61 +137,6 @@ $list-group-active-bg: $bg_primary; color: #666; } -.nav-compact > li > a { - padding: 6px 12px; -} - -.nav-small > li > a { - padding: 3px 5px; - font-size: 12px; -} - - -/* - * Callouts from Bootstrap3 docs - * - * Not quite alerts, but custom and helpful notes for folks reading the docs. - * Requires a base and modifier class. - */ - -/* Common styles for all types */ -.bs-callout { - margin: 20px 0; - padding: 20px; - border-left: 3px solid #eee; - color: #666; - background: #f9f9f9; -} -.bs-callout h4 { - margin-top: 0; - margin-bottom: 5px; -} -.bs-callout p:last-child { - margin-bottom: 0; -} - -/* Variations */ -.bs-callout-danger { - background-color: #fdf7f7; - border-color: #eed3d7; - color: #b94a48; -} -.bs-callout-warning { - background-color: #faf8f0; - border-color: #faebcc; - color: #8a6d3b; -} -.bs-callout-info { - background-color: #f4f8fa; - border-color: #bce8f1; - color: #34789a; -} -.bs-callout-success { - background-color: #dff0d8; - border-color: #5cA64d; - color: #3c763d; -} - /** * fix to keep tooltips position in top navigation bar * @@ -214,16 +151,13 @@ $list-group-active-bg: $bg_primary; * */ .panel { - @include border-radius(0px); - .panel-heading { - @include border-radius(0px); font-size: 14px; line-height: 18px; .panel-head-actions { position: relative; - top: -7px; + top: -6px; float: right; } } @@ -256,40 +190,10 @@ $list-group-active-bg: $bg_primary; } } -.panel-default { - .panel-heading { - background-color: #EEE; - } -} - -.panel-danger { - @include panel-colored; - .panel-heading { - color: $border_danger; - border-color: $border_danger; - } -} - -.panel-success { - @include panel-colored; - .panel-heading { - color: $border_success; - border-color: $border_success; - } -} - -.panel-primary { - @include panel-colored; - .panel-heading { - color: $border_primary; - border-color: $border_primary; - } -} - -.panel-warning { - @include panel-colored; - .panel-heading { - color: $border_warning; - border-color: $border_warning; +.alert { + a { + @extend .alert-link; + color: #fff; + text-decoration: underline; } } diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss new file mode 100644 index 0000000000..ea230646a8 --- /dev/null +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -0,0 +1,870 @@ +// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3): + +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +// $gray-base: #000 +// $gray-darker: lighten($gray-base, 13.5%) // #222 +// $gray-dark: lighten($gray-base, 20%) // #333 +// $gray: lighten($gray-base, 33.5%) // #555 +// $gray-light: lighten($gray-base, 46.7%) // #777 +// $gray-lighter: lighten($gray-base, 93.5%) // #eee +$gray-base: #000; +$gray-darker: lighten($gray-base, 13.5%); // #222 +$gray-dark: #7b8a8b; // #333 +$gray: #95a5a6; // #555 +$gray-light: #b4bcc2; // #999 +$gray-lighter: #ecf0f1; // #eee + +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: $gl-info; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +// $body-bg: #fff +//** Global text color on ``. +$text-color: $brand-primary; + +//** Global textual link color. +$link-color: $gl-link-color; +//** Link hover color set via `darken()` function. +// $link-hover-color: darken($link-color, 15%) +//** Link hover decoration. +// $link-hover-decoration: underline + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +// $font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif +// $font-family-serif: Georgia, "Times New Roman", Times, serif +//** Default monospace fonts for ``, ``, and `
`.
+// $font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace
+// $font-family-base:        $font-family-sans-serif
+
+$font-size-base:          $gl-font-size;
+// $font-size-large:         ceil(($font-size-base * 1.25)) // ~18px
+// $font-size-small:         ceil(($font-size-base * 0.85)) // ~12px
+
+// $font-size-h1:            floor(($font-size-base * 2.6)) // ~36px
+// $font-size-h2:            floor(($font-size-base * 2.15)) // ~30px
+// $font-size-h3:            ceil(($font-size-base * 1.7)) // ~24px
+// $font-size-h4:            ceil(($font-size-base * 1.25)) // ~18px
+// $font-size-h5:            $font-size-base
+// $font-size-h6:            ceil(($font-size-base * 0.85)) // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+// $line-height-base:        1.428571429 // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+// $line-height-computed:    floor(($font-size-base * $line-height-base)) // ~20px
+
+//** By default, this inherits from the ``.
+// $headings-font-family:    inherit
+// $headings-font-weight:    500
+// $headings-line-height:    1.1
+// $headings-color:          inherit
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+
+// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
+// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
+// $icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/")
+
+//** File name for all font files.
+// $icon-font-name:          "glyphicons-halflings-regular"
+//** Element ID within SVG icon file.
+// $icon-font-svg-id:        "glyphicons_halflingsregular"
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+$padding-base-vertical:     6px;
+$padding-base-horizontal:   14px;
+
+// $padding-large-vertical:    10px
+// $padding-large-horizontal:  16px
+
+// $padding-small-vertical:    5px
+// $padding-small-horizontal:  10px
+
+// $padding-xs-vertical:       1px
+// $padding-xs-horizontal:     5px
+
+// $line-height-large:         1.3333333 // extra decimals for Win 8.1 Chrome
+// $line-height-small:         1.5
+
+// $border-radius-base:        4px
+// $border-radius-large:       6px
+// $border-radius-small:       3px
+
+//** Global color for active items (e.g., navs or dropdowns).
+// $component-active-color:    #fff
+//** Global background color for active items (e.g., navs or dropdowns).
+// $component-active-bg:       $brand-primary
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+// $caret-width-base:          4px
+//** Carets increase slightly in size for larger components.
+// $caret-width-large:         5px
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+// $table-cell-padding:            8px
+//** Padding for cells in `.table-condensed`.
+// $table-condensed-cell-padding:  5px
+
+//** Default background color used for all tables.
+// $table-bg:                      transparent
+//** Background color used for `.table-striped`.
+// $table-bg-accent:               #f9f9f9
+//** Background color used for `.table-hover`.
+// $table-bg-hover:                #f5f5f5
+// $table-bg-active:               $table-bg-hover
+
+//** Border color for table and cell borders.
+// $table-border-color:            #ddd
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+// $btn-font-weight:                normal
+
+// $btn-default-color:              #333
+// $btn-default-bg:                 #fff
+// $btn-default-border:             #ccc
+
+// $btn-primary-color:              #fff
+// $btn-primary-bg:                 $brand-primary
+// $btn-primary-border:             darken($btn-primary-bg, 5%)
+
+// $btn-success-color:              #fff
+// $btn-success-bg:                 $brand-success
+// $btn-success-border:             darken($btn-success-bg, 5%)
+
+// $btn-info-color:                 #fff
+// $btn-info-bg:                    $brand-info
+// $btn-info-border:                darken($btn-info-bg, 5%)
+
+// $btn-warning-color:              #fff
+// $btn-warning-bg:                 $brand-warning
+// $btn-warning-border:             darken($btn-warning-bg, 5%)
+
+// $btn-danger-color:               #fff
+// $btn-danger-bg:                  $brand-danger
+// $btn-danger-border:              darken($btn-danger-bg, 5%)
+
+// $btn-link-disabled-color:        $gray-light
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+// $input-bg:                       #fff
+//** `` background color
+// $input-bg-disabled:              $gray-lighter
+
+//** Text color for ``s
+$input-color:                    $text-color;
+//** `` border color
+$input-border:                   #dce4ec;
+
+// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on ``s in CSS.
+// $input-border-radius:            $border-radius-base
+//** Large `.form-control` border radius
+// $input-border-radius-large:      $border-radius-large
+//** Small `.form-control` border radius
+// $input-border-radius-small:      $border-radius-small
+
+//** Border color for inputs on focus
+$input-border-focus:             $brand-info;
+
+//** Placeholder text color
+// $input-color-placeholder:        #999
+
+//** Default `.form-control` height
+// $input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2)
+//** Large `.form-control` height
+// $input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2)
+//** Small `.form-control` height
+// $input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2)
+
+$legend-color:                   $text-color;
+// $legend-border-color:            #e5e5e5
+
+//** Background color for textual input addons
+// $input-group-addon-bg:           $gray-lighter
+//** Border color for textual input addons
+// $input-group-addon-border-color: $input-border
+
+//** Disabled cursor for form controls and buttons.
+// $cursor-disabled:                not-allowed
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+// $dropdown-bg:                    #fff
+//** Dropdown menu `border-color`.
+// $dropdown-border:                rgba(0,0,0,.15)
+//** Dropdown menu `border-color` **for IE8**.
+// $dropdown-fallback-border:       #ccc
+//** Divider color for between dropdown items.
+// $dropdown-divider-bg:            #e5e5e5
+
+//** Dropdown link text color.
+// $dropdown-link-color:            $gray-dark
+//** Hover color for dropdown links.
+// $dropdown-link-hover-color:      darken($gray-dark, 5%)
+//** Hover background for dropdown links.
+// $dropdown-link-hover-bg:         #f5f5f5
+
+//** Active dropdown menu item text color.
+// $dropdown-link-active-color:     $component-active-color
+//** Active dropdown menu item background color.
+// $dropdown-link-active-bg:        $component-active-bg
+
+//** Disabled dropdown menu item background color.
+// $dropdown-link-disabled-color:   $gray-light
+
+//** Text color for headers within dropdown menus.
+// $dropdown-header-color:          $gray-light
+
+//** Deprecated `$dropdown-caret-color` as of v3.1.0
+// $dropdown-caret-color:           #000
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+// $zindex-navbar:            1000
+// $zindex-dropdown:          1000
+// $zindex-popover:           1060
+// $zindex-tooltip:           1070
+// $zindex-navbar-fixed:      1030
+// $zindex-modal:             1040
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `$screen-xs` as of v3.0.1
+// $screen-xs:                  480px
+//** Deprecated `$screen-xs-min` as of v3.2.0
+// $screen-xs-min:              $screen-xs
+//** Deprecated `$screen-phone` as of v3.0.1
+// $screen-phone:               $screen-xs-min
+
+// Small screen / tablet
+//** Deprecated `$screen-sm` as of v3.0.1
+// $screen-sm:                  768px
+// $screen-sm-min:              $screen-sm
+//** Deprecated `$screen-tablet` as of v3.0.1
+// $screen-tablet:              $screen-sm-min
+
+// Medium screen / desktop
+//** Deprecated `$screen-md` as of v3.0.1
+// $screen-md:                  992px
+// $screen-md-min:              $screen-md
+//** Deprecated `$screen-desktop` as of v3.0.1
+// $screen-desktop:             $screen-md-min
+
+// Large screen / wide desktop
+//** Deprecated `$screen-lg` as of v3.0.1
+// $screen-lg:                  1200px
+// $screen-lg-min:              $screen-lg
+//** Deprecated `$screen-lg-desktop` as of v3.0.1
+// $screen-lg-desktop:          $screen-lg-min
+
+// So media queries don't overlap when required, provide a maximum
+// $screen-xs-max:              ($screen-sm-min - 1)
+// $screen-sm-max:              ($screen-md-min - 1)
+// $screen-md-max:              ($screen-lg-min - 1)
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+// $grid-columns:              12
+//** Padding between columns. Gets divided in half for the left and right.
+// $grid-gutter-width:         30px
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+// $grid-float-breakpoint:     $screen-sm-min
+//** Point at which the navbar begins collapsing.
+// $grid-float-breakpoint-max: ($grid-float-breakpoint - 1)
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+// $container-tablet:             (720px + $grid-gutter-width)
+//** For `$screen-sm-min` and up.
+// $container-sm:                 $container-tablet
+
+// Medium screen / desktop
+// $container-desktop:            (940px + $grid-gutter-width)
+//** For `$screen-md-min` and up.
+// $container-md:                 $container-desktop
+
+// Large screen / wide desktop
+// $container-large-desktop:      (1140px + $grid-gutter-width)
+//** For `$screen-lg-min` and up.
+// $container-lg:                 $container-large-desktop
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+// $navbar-height:                    50px
+// $navbar-margin-bottom:             $line-height-computed
+// $navbar-border-radius:             $border-radius-base
+// $navbar-padding-horizontal:        floor(($grid-gutter-width / 2))
+// $navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2)
+// $navbar-collapse-max-height:       340px
+
+// $navbar-default-color:             #777
+// $navbar-default-bg:                #f8f8f8
+// $navbar-default-border:            darken($navbar-default-bg, 6.5%)
+
+// Navbar links
+// $navbar-default-link-color:                #777
+// $navbar-default-link-hover-color:          #333
+// $navbar-default-link-hover-bg:             transparent
+// $navbar-default-link-active-color:         #555
+// $navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%)
+// $navbar-default-link-disabled-color:       #ccc
+// $navbar-default-link-disabled-bg:          transparent
+
+// Navbar brand label
+// $navbar-default-brand-color:               $navbar-default-link-color
+// $navbar-default-brand-hover-color:         darken($navbar-default-brand-color, 10%)
+// $navbar-default-brand-hover-bg:            transparent
+
+// Navbar toggle
+// $navbar-default-toggle-hover-bg:           #ddd
+// $navbar-default-toggle-icon-bar-bg:        #888
+// $navbar-default-toggle-border-color:       #ddd
+
+
+// Inverted navbar
+// Reset inverted navbar basics
+// $navbar-inverse-color:                      lighten($gray-light, 15%)
+// $navbar-inverse-bg:                         #222
+// $navbar-inverse-border:                     darken($navbar-inverse-bg, 10%)
+
+// Inverted navbar links
+// $navbar-inverse-link-color:                 lighten($gray-light, 15%)
+// $navbar-inverse-link-hover-color:           #fff
+// $navbar-inverse-link-hover-bg:              transparent
+// $navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color
+// $navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%)
+// $navbar-inverse-link-disabled-color:        #444
+// $navbar-inverse-link-disabled-bg:           transparent
+
+// Inverted navbar brand label
+// $navbar-inverse-brand-color:                $navbar-inverse-link-color
+// $navbar-inverse-brand-hover-color:          #fff
+// $navbar-inverse-brand-hover-bg:             transparent
+
+// Inverted navbar toggle
+// $navbar-inverse-toggle-hover-bg:            #333
+// $navbar-inverse-toggle-icon-bar-bg:         #fff
+// $navbar-inverse-toggle-border-color:        #333
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+// $nav-link-padding:                          10px 15px
+// $nav-link-hover-bg:                         $gray-lighter
+
+// $nav-disabled-link-color:                   $gray-light
+// $nav-disabled-link-hover-color:             $gray-light
+
+//== Tabs
+// $nav-tabs-border-color:                     #ddd
+
+// $nav-tabs-link-hover-border-color:          $gray-lighter
+
+// $nav-tabs-active-link-hover-bg:             $body-bg
+// $nav-tabs-active-link-hover-color:          $gray
+// $nav-tabs-active-link-hover-border-color:   #ddd
+
+// $nav-tabs-justified-link-border-color:            #ddd
+// $nav-tabs-justified-active-link-border-color:     $body-bg
+
+//== Pills
+// $nav-pills-border-radius:                   $border-radius-base
+// $nav-pills-active-link-hover-bg:            $component-active-bg
+// $nav-pills-active-link-hover-color:         $component-active-color
+
+
+//== Pagination
+//
+//##
+
+$pagination-color:                     #fff;
+$pagination-bg:                        $brand-success;
+$pagination-border:                    transparent;
+
+$pagination-hover-color:               #fff;
+$pagination-hover-bg:                  darken($brand-success, 15%);
+$pagination-hover-border:              transparent;
+
+$pagination-active-color:              #fff;
+$pagination-active-bg:                 darken($brand-success, 15%);
+$pagination-active-border:             transparent;
+
+$pagination-disabled-color:            #b4bcc2;
+$pagination-disabled-bg:               lighten($brand-success, 15%);
+$pagination-disabled-border:           transparent;
+
+
+//== Pager
+//
+//##
+
+// $pager-bg:                             $pagination-bg
+// $pager-border:                         $pagination-border
+// $pager-border-radius:                  15px
+
+// $pager-hover-bg:                       $pagination-hover-bg
+
+// $pager-active-bg:                      $pagination-active-bg
+// $pager-active-color:                   $pagination-active-color
+
+// $pager-disabled-color:                 $pagination-disabled-color
+
+
+//== Jumbotron
+//
+//##
+
+// $jumbotron-padding:              30px
+// $jumbotron-color:                inherit
+// $jumbotron-bg:                   $gray-lighter
+// $jumbotron-heading-color:        inherit
+// $jumbotron-font-size:            ceil(($font-size-base * 1.5))
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+
+$state-success-text:             #fff;
+$state-success-bg:               $brand-success;
+$state-success-border:           $brand-success;
+
+$state-info-text:                #fff;
+$state-info-bg:                  $brand-info;
+$state-info-border:              $brand-info;
+
+$state-warning-text:             #fff;
+$state-warning-bg:               $brand-warning;
+$state-warning-border:           $brand-warning;
+
+$state-danger-text:              #fff;
+$state-danger-bg:                $brand-danger;
+$state-danger-border:            $brand-danger;
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+// $tooltip-max-width:           200px
+//** Tooltip text color
+// $tooltip-color:               #fff
+//** Tooltip background color
+// $tooltip-bg:                  #000
+// $tooltip-opacity:             .9
+
+//** Tooltip arrow width
+// $tooltip-arrow-width:         5px
+//** Tooltip arrow color
+// $tooltip-arrow-color:         $tooltip-bg
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+// $popover-bg:                          #fff
+//** Popover maximum width
+// $popover-max-width:                   276px
+//** Popover border color
+// $popover-border-color:                rgba(0,0,0,.2)
+//** Popover fallback border color
+// $popover-fallback-border-color:       #ccc
+
+//** Popover title background color
+// $popover-title-bg:                    darken($popover-bg, 3%)
+
+//** Popover arrow width
+// $popover-arrow-width:                 10px
+//** Popover arrow color
+// $popover-arrow-color:                 $popover-bg
+
+//** Popover outer arrow width
+// $popover-arrow-outer-width:           ($popover-arrow-width + 1)
+//** Popover outer arrow color
+// $popover-arrow-outer-color:           fade_in($popover-border-color, 0.05)
+//** Popover outer arrow fallback color
+// $popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%)
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+// $label-default-bg:            $gray-light
+//** Primary label background color
+// $label-primary-bg:            $brand-primary
+//** Success label background color
+// $label-success-bg:            $brand-success
+//** Info label background color
+// $label-info-bg:               $brand-info
+//** Warning label background color
+// $label-warning-bg:            $brand-warning
+//** Danger label background color
+// $label-danger-bg:             $brand-danger
+
+//** Default label text color
+// $label-color:                 #fff
+//** Default text color of a linked label
+// $label-link-hover-color:      #fff
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+// $modal-inner-padding:         15px
+
+//** Padding applied to the modal title
+// $modal-title-padding:         15px
+//** Modal title line-height
+// $modal-title-line-height:     $line-height-base
+
+//** Background color of modal content area
+// $modal-content-bg:                             #fff
+//** Modal content border color
+// $modal-content-border-color:                   rgba(0,0,0,.2)
+//** Modal content border color **for IE8**
+// $modal-content-fallback-border-color:          #999
+
+//** Modal backdrop background color
+// $modal-backdrop-bg:           #000
+//** Modal backdrop opacity
+// $modal-backdrop-opacity:      .5
+//** Modal header border color
+// $modal-header-border-color:   #e5e5e5
+//** Modal footer border color
+// $modal-footer-border-color:   $modal-header-border-color
+
+// $modal-lg:                    900px
+// $modal-md:                    600px
+// $modal-sm:                    300px
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+// $alert-padding:               15px
+$alert-border-radius:            0;
+// $alert-link-font-weight:      bold
+
+// $alert-success-bg:            $state-success-bg
+// $alert-success-text:          $state-success-text
+// $alert-success-border:        $state-success-border
+
+// $alert-info-bg:               $state-info-bg
+// $alert-info-text:             $state-info-text
+// $alert-info-border:           $state-info-border
+
+// $alert-warning-bg:            $state-warning-bg
+// $alert-warning-text:          $state-warning-text
+// $alert-warning-border:        $state-warning-border
+
+// $alert-danger-bg:             $state-danger-bg
+// $alert-danger-text:           $state-danger-text
+// $alert-danger-border:         $state-danger-border
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+// $progress-bg:                 #f5f5f5
+//** Progress bar text color
+// $progress-bar-color:          #fff
+//** Variable for setting rounded corners on progress bar.
+// $progress-border-radius:      $border-radius-base
+
+//** Default progress bar color
+// $progress-bar-bg:             $brand-primary
+//** Success progress bar color
+// $progress-bar-success-bg:     $brand-success
+//** Warning progress bar color
+// $progress-bar-warning-bg:     $brand-warning
+//** Danger progress bar color
+// $progress-bar-danger-bg:      $brand-danger
+//** Info progress bar color
+// $progress-bar-info-bg:        $brand-info
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+// $list-group-bg:                 #fff
+//** `.list-group-item` border color
+// $list-group-border:             #ddd
+//** List group border radius
+// $list-group-border-radius:      $border-radius-base
+
+//** Background color of single list items on hover
+// $list-group-hover-bg:           #f5f5f5
+//** Text color of active list items
+// $list-group-active-color:       $component-active-color
+//** Background color of active list items
+// $list-group-active-bg:          $component-active-bg
+//** Border color of active list elements
+// $list-group-active-border:      $list-group-active-bg
+//** Text color for content within active list items
+// $list-group-active-text-color:  lighten($list-group-active-bg, 40%)
+
+//** Text color of disabled list items
+// $list-group-disabled-color:      $gray-light
+//** Background color of disabled list items
+// $list-group-disabled-bg:         $gray-lighter
+//** Text color for content within disabled list items
+// $list-group-disabled-text-color: $list-group-disabled-color
+
+// $list-group-link-color:         #555
+// $list-group-link-hover-color:   $list-group-link-color
+// $list-group-link-heading-color: #333
+
+
+//== Panels
+//
+//##
+
+// $panel-bg:                    #fff
+// $panel-body-padding:          15px
+// $panel-heading-padding:       10px 15px
+// $panel-footer-padding:        $panel-heading-padding
+$panel-border-radius:            0;
+
+//** Border color for elements within panels
+// $panel-inner-border:          #ddd
+// $panel-footer-bg:             #f5f5f5
+
+$panel-default-text:          $text-color;
+// $panel-default-border:        #ddd
+// $panel-default-heading-bg:    #f5f5f5
+
+// $panel-primary-text:          #fff
+// $panel-primary-border:        $brand-primary
+// $panel-primary-heading-bg:    $brand-primary
+
+// $panel-success-text:          $state-success-text
+// $panel-success-border:        $state-success-border
+// $panel-success-heading-bg:    $state-success-bg
+
+// $panel-info-text:             $state-info-text
+// $panel-info-border:           $state-info-border
+// $panel-info-heading-bg:       $state-info-bg
+
+// $panel-warning-text:          $state-warning-text
+// $panel-warning-border:        $state-warning-border
+// $panel-warning-heading-bg:    $state-warning-bg
+
+// $panel-danger-text:           $state-danger-text
+// $panel-danger-border:         $state-danger-border
+// $panel-danger-heading-bg:     $state-danger-bg
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+// $thumbnail-padding:           4px
+//** Thumbnail background color
+// $thumbnail-bg:                $body-bg
+//** Thumbnail border color
+// $thumbnail-border:            #ddd
+//** Thumbnail border radius
+// $thumbnail-border-radius:     $border-radius-base
+
+//** Custom text color for thumbnail captions
+// $thumbnail-caption-color:     $text-color
+//** Padding around the thumbnail caption
+// $thumbnail-caption-padding:   9px
+
+
+//== Wells
+//
+//##
+
+$well-bg:                     $gray-lighter;
+$well-border:                 transparent;
+
+
+//== Badges
+//
+//##
+
+// $badge-color:                 #fff
+//** Linked badge text color on hover
+// $badge-link-hover-color:      #fff
+// $badge-bg:                    $gray-light
+
+//** Badge text color in active nav link
+// $badge-active-color:          $link-color
+//** Badge background color in active nav link
+// $badge-active-bg:             #fff
+
+// $badge-font-weight:           bold
+// $badge-line-height:           1
+// $badge-border-radius:         10px
+
+
+//== Breadcrumbs
+//
+//##
+
+// $breadcrumb-padding-vertical:   8px
+// $breadcrumb-padding-horizontal: 15px
+//** Breadcrumb background color
+// $breadcrumb-bg:                 #f5f5f5
+//** Breadcrumb text color
+// $breadcrumb-color:              #ccc
+//** Text color of current page in the breadcrumb
+// $breadcrumb-active-color:       $gray-light
+//** Textual separator for between breadcrumb elements
+// $breadcrumb-separator:          "/"
+
+
+//== Carousel
+//
+//##
+
+// $carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6)
+
+// $carousel-control-color:                      #fff
+// $carousel-control-width:                      15%
+// $carousel-control-opacity:                    .5
+// $carousel-control-font-size:                  20px
+
+// $carousel-indicator-active-bg:                #fff
+// $carousel-indicator-border-color:             #fff
+
+// $carousel-caption-color:                      #fff
+
+
+//== Close
+//
+//##
+
+// $close-font-weight:           bold
+// $close-color:                 #000
+// $close-text-shadow:           0 1px 0 #fff
+
+
+//== Code
+//
+//##
+
+$code-color:                  #c7254e;
+$code-bg:                     #f9f2f4;
+
+$kbd-color:                   #fff;
+$kbd-bg:                      #333;
+
+$pre-bg:                      $gray-lighter;
+$pre-color:                   $text-color;
+$pre-border-color:            #ccc;
+// $pre-scrollable-max-height:   340px
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+// $component-offset-horizontal: 180px
+//** Text muted color
+// $text-muted:                  $gray-light
+//** Abbreviations and acronyms border color
+// $abbr-border-color:           $gray-light
+//** Headings small color
+$headings-small-color:        $gray-dark;
+//** Blockquote small color
+// $blockquote-small-color:      $gray-light
+//** Blockquote font size
+// $blockquote-font-size:        ($font-size-base * 1.25)
+//** Blockquote border color
+// $blockquote-border-color:     $gray-lighter
+//** Page header border color
+// $page-header-border-color:    $gray-lighter
+//** Width of horizontal description list titles
+// $dl-horizontal-offset:        $component-offset-horizontal
+//** Horizontal line color.
+// $hr-border:                   $gray-lighter
diff --git a/app/assets/stylesheets/main/layout.scss b/app/assets/stylesheets/base/layout.scss
similarity index 90%
rename from app/assets/stylesheets/main/layout.scss
rename to app/assets/stylesheets/base/layout.scss
index 1085e68b7d..62c11b0636 100644
--- a/app/assets/stylesheets/main/layout.scss
+++ b/app/assets/stylesheets/base/layout.scss
@@ -4,7 +4,7 @@ html {
   &.touch .tooltip { display: none !important; }
 
   body {
-    padding-top: 47px;
+    padding-top: 46px;
   }
 }
 
diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/base/mixins.scss
similarity index 87%
rename from app/assets/stylesheets/main/mixins.scss
rename to app/assets/stylesheets/base/mixins.scss
index e54482d14c..ccba65e3fd 100644
--- a/app/assets/stylesheets/main/mixins.scss
+++ b/app/assets/stylesheets/base/mixins.scss
@@ -121,14 +121,6 @@
   }
 }
 
-@mixin page-title {
-  color: #333;
-  line-height: 1.5;
-  font-weight: normal;
-  margin-top: 0px;
-  margin-bottom: 10px;
-}
-
 @mixin str-truncated($max_width: 82%) {
   display: inline-block;
   overflow: hidden;
@@ -137,14 +129,3 @@
   white-space: nowrap;
   max-width: $max_width;
 }
-
-@mixin panel-colored {
-  border: 1px solid #EEE;
-  background: $box_bg;
-  @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09));
-
-  .panel-heading {
-    font-weight: bold;
-    background-color: $box_bg;
-  }
-}
diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss
new file mode 100644
index 0000000000..54af78ee08
--- /dev/null
+++ b/app/assets/stylesheets/base/variables.scss
@@ -0,0 +1,37 @@
+$style_color: #474D57;
+$hover: #FFF3EB;
+$box_bg: #F9F9F9;
+$gl-link-color: #446e9b;
+$nprogress-color: #c0392b;
+$gl-font-size: 14px;
+$list-font-size: 15px;
+$sidebar_width: 230px;
+$avatar_radius: 50%;
+$code_font_size: 13px;
+$code_line_height: 1.5;
+
+/*
+ * State colors:
+ */
+$gl-success: #019875;
+$gl-danger: #d9534f;
+$gl-primary: #446e9b;
+$gl-info: #029ACF;
+$gl-warning: #EB9532;
+
+$gl-primary:         #2C3E50;
+$gl-success:         #18BC9C;
+$gl-info:            #3498DB;
+$gl-warning:         #F39C12;
+$gl-danger:          #E74C3C;
+/*
+ * Commit Diff Colors
+ */
+$added: #63c363;
+$deleted: #f77;
+
+/*
+ * Fonts
+ */
+$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
+$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif;
diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss
index 3b36027506..0224484d82 100644
--- a/app/assets/stylesheets/generic/buttons.scss
+++ b/app/assets/stylesheets/generic/buttons.scss
@@ -1,127 +1,15 @@
 .btn {
-  display: inline-block;
-  margin-bottom: 0;
-  font-weight: normal;
-  text-align: center;
-  vertical-align: middle;
-  cursor: pointer;
-  background-image: none;
-  border: $btn-border;
-  white-space: nowrap;
-  padding: 6px 12px;
-  font-size: 13px;
-  line-height: 18px;
-  border-radius: 4px;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  -o-user-select: none;
-  user-select: none;
-  color: #444444;
-  background-color: #fff;
-  text-shadow: none;
-
-  &.hover,
-  &:hover {
-    color: #444444;
-    text-decoration: none;
-    background-color: #ebebeb;
-    border-color: #adadad;
-  }
-
-  &.focus,
-  &:focus {
-    color: #444444;
-    text-decoration: none;
-    outline: thin dotted #333;
-    outline: 5px auto -webkit-focus-ring-color;
-    outline-offset: -2px;
-  }
-
-  &.active,
-  &:active {
-    outline: 0;
-    background-image: none;
-    -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
-    box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
-  }
-
-  &.disabled,
-  &[disabled] {
-    cursor: not-allowed;
-    pointer-events: none;
-    opacity: 0.65;
-    filter: alpha(opacity=65);
-    -webkit-box-shadow: none;
-    box-shadow: none;
-  }
-
-  &.btn-primary {
-    color: #ffffff;
-    background-color: $bg_primary;
-    border-color: $border_primary;
-
-    &.hover,
-    &:hover,
-    &.disabled,
-    &[disabled] {
-      color: #ffffff;
-    }
-  }
-
-  &.btn-success {
-    color: #ffffff;
-    background-color: $bg_success;
-    border-color: $border_success;
-
-
-    &.hover,
-    &:hover,
-    &.disabled,
-    &[disabled] {
-      color: #ffffff;
-    }
-  }
-
-  &.btn-danger {
-    color: #ffffff;
-    background-color: $bg_danger;
-    border-color: $border_danger;
-
-
-    &.hover,
-    &:hover,
-    &.disabled,
-    &[disabled] {
-      color: #ffffff;
-    }
-  }
-
-  &.btn-warning {
-    color: #ffffff;
-    background-color: $bg_warning;
-    border-color: $border_warning;
-
-
-    &.hover,
-    &:hover,
-    &.disabled,
-    &[disabled] {
-      color: #ffffff;
-    }
-  }
+  @extend .btn-default;
 
   &.btn-new {
     @extend .btn-success;
   }
 
   &.btn-create {
-    @extend .wide;
     @extend .btn-success;
   }
 
   &.btn-save {
-    @extend .wide;
     @extend .btn-primary;
   }
 
@@ -133,11 +21,6 @@
     float: right;
   }
 
-  &.wide {
-    padding-left: 20px;
-    padding-right: 20px;
-  }
-
   &.btn-small {
     padding: 2px 10px;
     font-size: 12px;
@@ -151,16 +34,16 @@
   }
 
   &.btn-close {
-    color: $bg_danger;
-    border-color: $border_danger;
+    color: $gl-danger;
+    border-color: $gl-danger;
     &:hover {
       color: #B94A48;
     }
   }
 
   &.btn-reopen {
-    color: $bg_success;
-    border-color: $border_success;
+    color: $gl-success;
+    border-color: $gl-success;
     &:hover {
       color: #468847;
     }
@@ -174,9 +57,12 @@
     }
   }
 
-  &.btn-lg {
-    font-size: 15px;
-    line-height: 1.4;
+  &.btn-save {
+    @extend .btn-primary;
+  }
+
+  &.btn-new, &.btn-create {
+    @extend .btn-success;
   }
 }
 
diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss
index aca5c84524..df0f150f40 100644
--- a/app/assets/stylesheets/generic/common.scss
+++ b/app/assets/stylesheets/generic/common.scss
@@ -24,7 +24,7 @@
 
 .slead {
   color: #666;
-  font-size: 14px;
+  font-size: 15px;
   margin-bottom: 12px;
   font-weight: normal;
   line-height: 24px;
@@ -61,7 +61,7 @@ pre {
 
 .dropdown-menu > li > a:hover,
 .dropdown-menu > li > a:focus {
-  background: $bg_primary;
+  background: $gl-primary;
   color: #FFF
 }
 
@@ -71,7 +71,7 @@ pre {
 
 /** FLASH message **/
 .author_link {
-  color: $link_color;
+  color: $gl-link-color;
 }
 
 .help li { color:$style_color; }
@@ -315,20 +315,8 @@ table {
   width: 100%;
 }
 
-.broadcast-message {
-  padding: 10px;
-  text-align: center;
-  background: #555;
-  color: #BBB;
-}
-
-.broadcast-message-preview {
-  @extend .broadcast-message;
-  margin-bottom: 20px;
-}
-
 .btn-sign-in {
-  margin-top: 7px;
+  margin-top: 5px;
   text-shadow: none;
 }
 
@@ -346,8 +334,11 @@ table {
   overflow-x: auto;
 }
 
-.footer-links a {
-  margin-right: 15px;
+.footer-links {
+  margin-bottom: 20px;
+  a {
+    margin-right: 15px;
+  }
 }
 
 .search_box {
diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss
index c8982cdc00..19bc11086e 100644
--- a/app/assets/stylesheets/generic/forms.scss
+++ b/app/assets/stylesheets/generic/forms.scss
@@ -29,7 +29,7 @@ fieldset legend {
   padding: 17px 20px 18px;
   margin-top: 18px;
   margin-bottom: 18px;
-  background-color: whitesmoke;
+  background-color: #ecf0f1;
   border-top: 1px solid #e5e5e5;
 }
 
diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/generic/gfm.scss
index 1427b6a5ae..8fac5e534f 100644
--- a/app/assets/stylesheets/generic/gfm.scss
+++ b/app/assets/stylesheets/generic/gfm.scss
@@ -3,7 +3,7 @@
  */
 .issue-form, .merge-request-form, .wiki-form {
   .description {
-    height: 20em;
+    height: 16em;
     border-top-left-radius: 0;
   }
 }
diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss
index 2563ab516e..9558f241b7 100644
--- a/app/assets/stylesheets/generic/issue_box.scss
+++ b/app/assets/stylesheets/generic/issue_box.scss
@@ -11,17 +11,17 @@
   margin-right: 5px;
 
   &.issue-box-closed {
-    background-color: $bg_danger;
+    background-color: $gl-danger;
     color: #FFF;
   }
 
   &.issue-box-merged {
-    background-color: $bg_primary;
+    background-color: $gl-primary;
     color: #FFF;
   }
 
   &.issue-box-open {
-    background-color: $bg_success;
+    background-color: $gl-success;
     color: #FFF;
   }
 
diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/generic/jquery.scss
index bfbbc7d25e..871b808bad 100644
--- a/app/assets/stylesheets/generic/jquery.scss
+++ b/app/assets/stylesheets/generic/jquery.scss
@@ -41,8 +41,8 @@
   }
 
   .ui-state-active {
-    border: 1px solid $bg_primary;
-    background: $bg_primary;
+    border: 1px solid $gl-primary;
+    background: $gl-primary;
     color: #FFF;
   }
 
diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss
index 5a87cc6c61..eb39b6bb7e 100644
--- a/app/assets/stylesheets/generic/markdown_area.scss
+++ b/app/assets/stylesheets/generic/markdown_area.scss
@@ -57,7 +57,6 @@
   border: 1px solid #ddd;
   min-height: 100px;
   padding: 5px;
-  font-size: 14px;
   box-shadow: none;
 }
 
@@ -77,3 +76,12 @@
     }
   }
 }
+
+.markdown-area {
+  background: #FFF;
+  border: 1px solid #ddd;
+  min-height: 100px;
+  padding: 5px;
+  box-shadow: none;
+  width: 100%;
+}
diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss
index b3727c3367..1b0e056216 100644
--- a/app/assets/stylesheets/generic/mobile.scss
+++ b/app/assets/stylesheets/generic/mobile.scss
@@ -43,8 +43,10 @@
     }
   }
 
-  .page-title .new-issue-link {
-    display: none;
+  .page-title {
+    .note_created_ago, .new-issue-link {
+      display: none;
+    }
   }
 
   .issue_edited_ago, .note_edited_ago {
diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss
similarity index 98%
rename from app/assets/stylesheets/sections/nav_sidebar.scss
rename to app/assets/stylesheets/generic/nav_sidebar.scss
index 335f137966..4bf2c609be 100644
--- a/app/assets/stylesheets/sections/nav_sidebar.scss
+++ b/app/assets/stylesheets/generic/nav_sidebar.scss
@@ -154,9 +154,10 @@
 
 .collapse-nav a {
   position: fixed;
-  top: 47px;
+  top: 46px;
   padding: 5px 13px 3px 13px;
   left: 197px;
+  font-size: 13px;
   background: #EEE;
   color: black;
   border: 1px solid rgba(0,0,0,0.035);
diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss
index 5d2f054d8f..ff08641263 100644
--- a/app/assets/stylesheets/generic/selects.scss
+++ b/app/assets/stylesheets/generic/selects.scss
@@ -3,9 +3,8 @@
   .select2-choice {
     background: #FFF;
     border-color: #BBB;
-    padding: 6px 12px;
-    font-size: 13px;
-    line-height: 18px;
+    padding: 6px 14px;
+    line-height: 1.42857143;
     height: auto;
 
     .select2-arrow {
@@ -20,7 +19,7 @@
 }
 
 .select2-container-multi .select2-choices .select2-search-field input {
-  padding: 6px 12px;
+  padding: 8px 14px;
   font-size: 13px;
   line-height: 18px;
   height: auto;
@@ -42,7 +41,7 @@
   .select2-results {
     max-height: 350px;
     .select2-highlighted {
-      background: $bg_primary;
+      background: $gl-primary;
     }
   }
 }
diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss
index c547ebb3aa..4d940ee6b2 100644
--- a/app/assets/stylesheets/generic/typography.scss
+++ b/app/assets/stylesheets/generic/typography.scss
@@ -2,28 +2,12 @@
  * Headers
  *
  */
-h1.page-title {
-  @include page-title;
-  font-size: 28px;
-}
-
-h2.page-title {
-  @include page-title;
-  font-size: 24px;
-}
-
-h3.page-title {
-  @include page-title;
-  font-size: 22px;
-}
-
-h4.page-title {
+.page-title {
   margin-top: 0px;
-}
-
-h6 {
-  color: #888;
-  text-transform: uppercase;
+  color: #333;
+  line-height: 1.5;
+  font-weight: normal;
+  margin-bottom: 5px;
 }
 
 /** CODE **/
@@ -36,52 +20,6 @@ pre {
   }
 }
 
-/**
- * Links
- *
- */
-a {
-  outline: none;
-  color: $link_color;
-  &:hover {
-    text-decoration: underline;
-    color: $link_hover_color;
-  }
-
-  &:focus {
-    text-decoration: underline;
-  }
-
-  &.darken {
-    color: $style_color;
-  }
-
-  &.lined {
-    text-decoration: underline;
-    &:hover { text-decoration: underline; }
-  }
-
-  &.gray {
-    color: gray;
-  }
-
-  &.supp_diff_link {
-    text-align: center;
-    padding: 20px 0;
-    background: #f1f1f1;
-    width: 100%;
-    float: left;
-  }
-
-  &.neib  {
-    margin-right: 15px;
-  }
-}
-
-a:focus {
-  outline: none;
-}
-
 .monospace {
   font-family: $monospace_font;
 }
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index fcd4d47bac..c8cb18ec35 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,6 +1,10 @@
 /* https://github.com/MozMorris/tomorrow-pygments */
+pre.code.highlight.dark,
 .code.dark {
 
+  background-color: #1d1f21;
+  color: #c5c8c6;
+
   pre.code,
   .line-numbers,
   .line-numbers a {
@@ -13,8 +17,8 @@
   }
 
   // highlight line via anchor
-  pre.hll {
-    background-color: #fff !important;
+  pre .hll {
+    background-color: #557 !important;
   }
 
   .hll { background-color: #373b41 }
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index bcd2e71665..001e8b3102 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,6 +1,10 @@
 /* https://github.com/richleland/pygments-css/blob/master/monokai.css */
+pre.code.monokai,
 .code.monokai {
 
+  background: #272822;
+  color: #f8f8f2;
+
   pre.highlight,
   .line-numbers,
   .line-numbers a {
@@ -13,7 +17,7 @@
   }
 
   // highlight line via anchor
-  pre.hll {
+  pre .hll {
     background-color: #49483e !important;
   }
 
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 4a6b759bd2..f5b827e7c0 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,6 +1,10 @@
 /* https://gist.github.com/qguv/7936275 */
+pre.code.highlight.solarized-dark,
 .code.solarized-dark {
 
+  background-color: #002b36;
+  color: #93a1a1;
+
   pre.code,
   .line-numbers,
   .line-numbers a {
@@ -13,8 +17,8 @@
   }
 
   // highlight line via anchor
-  pre.hll {
-    background-color: #073642 !important;
+  pre .hll {
+    background-color: #174652 !important;
   }
 
   /* Solarized Dark
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 7254f4d7ac..6b44c00c30 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,6 +1,10 @@
 /* https://gist.github.com/qguv/7936275 */
+pre.code.highlight.solarized-light,
 .code.solarized-light {
 
+  background-color: #fdf6e3;
+  color: #586e75;
+
   pre.code,
   .line-numbers,
   .line-numbers a {
@@ -13,8 +17,8 @@
   }
 
   // highlight line via anchor
-  pre.hll {
-    background-color: #eee8d5 !important;
+  pre .hll {
+    background-color: #ddd8c5 !important;
   }
 
   /* Solarized Light
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 4d6f5dfd91..a52ffc971d 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,6 +1,10 @@
 /* https://github.com/aahan/pygments-github-style */
+pre.code.highlight.white,
 .code.white {
 
+  background-color: #fff;
+  color: #333;
+
   pre.highlight,
   .line-numbers,
   .line-numbers a {
@@ -13,7 +17,7 @@
   }
 
   // highlight line via anchor
-  pre.hll {
+  pre .hll {
     background-color: #f8eec7 !important;
   }
 
diff --git a/app/assets/stylesheets/main/fonts.scss b/app/assets/stylesheets/main/fonts.scss
deleted file mode 100644
index f945aaca84..0000000000
--- a/app/assets/stylesheets/main/fonts.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-/** Typo **/
-$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif;
diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss
deleted file mode 100644
index acbf5be94a..0000000000
--- a/app/assets/stylesheets/main/variables.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * General Colors
- */
-$style_color: #474D57;
-$hover: #FFF3EB;
-$box_bg: #F9F9F9;
-
-/*
- * Link colors
- */
-$link_color: #446e9b;
-$link_hover_color: darken($link-color, 10%);
-
-$btn-border: 1px solid #ccc;
-
-/*
- * Success colors (green)
- */
-$border_success: #019875;
-$bg_success: #019875;
-
-/*
- * Danger colors (red)
- */
-$border_danger: #d43f3a;
-$bg_danger: #d9534f;
-
-/*
- * Primary colors (blue)
- */
-$border_primary: #446e9b;
-$bg_primary: #446e9b;
-
-/*
- * Warning colors (yellow)
- */
-$bg_warning: #EB9532;
-$border_warning: #EB9532;
-
-/**
- * Commit Diff Colors
- */
-$added: #63c363;
-$deleted: #f77;
-
-/**
- * NProgress customize
- */
-$nprogress-color: #c0392b;
-
-/**
- * Font sizes
- */
-$list-font-size: 15px;
-
-/**
- * Sidebar navigation width
- */
-$sidebar_width: 230px;
-
-$avatar_radius: 50%;
-$code_font_size: 13px;
-$code_line_height: 1.5;
diff --git a/app/assets/stylesheets/sections/admin.scss b/app/assets/stylesheets/pages/admin.scss
similarity index 78%
rename from app/assets/stylesheets/sections/admin.scss
rename to app/assets/stylesheets/pages/admin.scss
index a51deee797..144852e787 100644
--- a/app/assets/stylesheets/sections/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -50,3 +50,14 @@
     line-height: 2;
   }
 }
+
+.broadcast-message {
+  @extend .alert-warning;
+  padding: 10px;
+  text-align: center;
+}
+
+.broadcast-message-preview {
+  @extend .broadcast-message;
+  margin-bottom: 20px;
+}
diff --git a/app/assets/stylesheets/sections/commit.scss b/app/assets/stylesheets/pages/commit.scss
similarity index 94%
rename from app/assets/stylesheets/sections/commit.scss
rename to app/assets/stylesheets/pages/commit.scss
index 0e2d9571a4..f46d6542c0 100644
--- a/app/assets/stylesheets/sections/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -45,15 +45,6 @@
   }
 }
 
-.commit-committer-link,
-.commit-author-link {
-  font-size: 13px;
-  color: #555;
-  &:hover {
-    color: #999;
-  }
-}
-
 .commit-box {
   margin: 10px 0;
   border-top: 1px solid #ddd;
diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/pages/commits.scss
similarity index 98%
rename from app/assets/stylesheets/sections/commits.scss
rename to app/assets/stylesheets/pages/commits.scss
index 683aca7359..e167d044e4 100644
--- a/app/assets/stylesheets/sections/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -100,6 +100,7 @@ li.commit {
   .commit-row-info {
     color: #777;
     line-height: 24px;
+    font-size: 13px;
 
     a {
       color: #777;
diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
similarity index 71%
rename from app/assets/stylesheets/sections/dashboard.scss
rename to app/assets/stylesheets/pages/dashboard.scss
index d8fd83d44b..5a543a852c 100644
--- a/app/assets/stylesheets/sections/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -23,28 +23,6 @@
   }
 }
 
-.dash-sidebar-tabs {
-  margin-bottom: 2px;
-  border: none;
-  margin: 0 !important;
-
-  li {
-    &.active {
-      a {
-        background-color: #EEE;
-        border-bottom: 1px solid #EEE !important;
-        &:hover {
-          background: #eee;
-        }
-      }
-    }
-
-    a {
-      border-color: #DDD !important;
-    }
-  }
-}
-
 .project-row, .group-row {
   padding: 0 !important;
   font-size: 14px;
@@ -111,17 +89,8 @@
 }
 
 .dash-new-project {
-  background: $bg_success;
-  border: 1px solid $border_success;
-
-  a {
-    color: #FFF;
-  }
-}
-
-.dash-new-group {
-  background: $bg_success;
-  border: 1px solid $border_success;
+  background: $gl-success;
+  border: 1px solid $gl-success;
 
   a {
     color: #FFF;
diff --git a/app/assets/stylesheets/sections/diff.scss b/app/assets/stylesheets/pages/diff.scss
similarity index 100%
rename from app/assets/stylesheets/sections/diff.scss
rename to app/assets/stylesheets/pages/diff.scss
diff --git a/app/assets/stylesheets/sections/editor.scss b/app/assets/stylesheets/pages/editor.scss
similarity index 100%
rename from app/assets/stylesheets/sections/editor.scss
rename to app/assets/stylesheets/pages/editor.scss
diff --git a/app/assets/stylesheets/sections/errors.scss b/app/assets/stylesheets/pages/errors.scss
similarity index 100%
rename from app/assets/stylesheets/sections/errors.scss
rename to app/assets/stylesheets/pages/errors.scss
diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/pages/events.scss
similarity index 98%
rename from app/assets/stylesheets/sections/events.scss
rename to app/assets/stylesheets/pages/events.scss
index a477359dc8..3e9e36e477 100644
--- a/app/assets/stylesheets/sections/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -46,7 +46,6 @@
   border-bottom: 1px solid #eee;
   .event-title {
     @include str-truncated(72%);
-    color: #333;
     font-weight: 500;
     font-size: 14px;
     .author_name {
@@ -54,6 +53,7 @@
     }
   }
   .event-body {
+    font-size: 13px;
     margin-left: 35px;
     margin-right: 80px;
     color: #777;
@@ -185,11 +185,10 @@
 }
 
 .event_filter {
-  
   li a {
+    font-size: 13px;
     padding: 5px 10px;
     background: rgba(0,0,0,0.045);
     margin-left: 4px;
   }
-
 }
diff --git a/app/assets/stylesheets/sections/explore.scss b/app/assets/stylesheets/pages/explore.scss
similarity index 100%
rename from app/assets/stylesheets/sections/explore.scss
rename to app/assets/stylesheets/pages/explore.scss
diff --git a/app/assets/stylesheets/sections/graph.scss b/app/assets/stylesheets/pages/graph.scss
similarity index 100%
rename from app/assets/stylesheets/sections/graph.scss
rename to app/assets/stylesheets/pages/graph.scss
diff --git a/app/assets/stylesheets/sections/groups.scss b/app/assets/stylesheets/pages/groups.scss
similarity index 100%
rename from app/assets/stylesheets/sections/groups.scss
rename to app/assets/stylesheets/pages/groups.scss
diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/pages/header.scss
similarity index 100%
rename from app/assets/stylesheets/sections/header.scss
rename to app/assets/stylesheets/pages/header.scss
diff --git a/app/assets/stylesheets/sections/help.scss b/app/assets/stylesheets/pages/help.scss
similarity index 95%
rename from app/assets/stylesheets/sections/help.scss
rename to app/assets/stylesheets/pages/help.scss
index 07c62f98c3..6da7a2511a 100644
--- a/app/assets/stylesheets/sections/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -12,7 +12,6 @@
     color: #888;
 
     a {
-      font-size: 14px;
       margin-right: 3px;
     }
   }
@@ -29,7 +28,6 @@
 
   th {
     padding-top: 15px;
-    font-size: 14px;
     line-height: 1.5;
     color: #333;
     text-align: left
diff --git a/app/assets/stylesheets/sections/import.scss b/app/assets/stylesheets/pages/import.scss
similarity index 100%
rename from app/assets/stylesheets/sections/import.scss
rename to app/assets/stylesheets/pages/import.scss
diff --git a/app/assets/stylesheets/sections/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
similarity index 100%
rename from app/assets/stylesheets/sections/issuable.scss
rename to app/assets/stylesheets/pages/issuable.scss
diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/pages/issues.scss
similarity index 98%
rename from app/assets/stylesheets/sections/issues.scss
rename to app/assets/stylesheets/pages/issues.scss
index b909725bff..4ea34cc1da 100644
--- a/app/assets/stylesheets/sections/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -11,6 +11,7 @@
 
     .issue-info {
       color: #999;
+      font-size: 13px;
     }
 
     .issue-check {
@@ -40,7 +41,7 @@
 }
 
 .check-all-holder {
-  height: 32px;
+  height: 36px;
   float: left;
   margin-right: 12px;
   padding: 6px 15px;
diff --git a/app/assets/stylesheets/sections/labels.scss b/app/assets/stylesheets/pages/labels.scss
similarity index 100%
rename from app/assets/stylesheets/sections/labels.scss
rename to app/assets/stylesheets/pages/labels.scss
diff --git a/app/assets/stylesheets/sections/login.scss b/app/assets/stylesheets/pages/login.scss
similarity index 100%
rename from app/assets/stylesheets/sections/login.scss
rename to app/assets/stylesheets/pages/login.scss
diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
similarity index 92%
rename from app/assets/stylesheets/sections/merge_requests.scss
rename to app/assets/stylesheets/pages/merge_requests.scss
index 0d2d8b0173..9bd34b7376 100644
--- a/app/assets/stylesheets/sections/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -96,6 +96,7 @@
 
     .merge-request-info {
       color: #999;
+      font-size: 13px;
 
       .merge-request-labels {
         display: inline-block;
@@ -136,8 +137,8 @@
     background-color: #F5F5F5;
 
     &.ci-success {
-      color: $bg_success;
-      border-color: $border_success;
+      color: $gl-success;
+      border-color: $gl-success;
       background-color: #F1FAF1;
     }
 
@@ -148,20 +149,20 @@
     }
 
     &.ci-running {
-      color: $bg_warning;
-      border-color: $border_warning;
+      color: $gl-warning;
+      border-color: $gl-warning;
       background-color: #FAF5F1;
     }
 
     &.ci-failed {
-      color: $bg_danger;
-      border-color: $border_danger;
+      color: $gl-danger;
+      border-color: $gl-danger;
       background-color: #FAF1F1;
     }
 
     &.ci-error {
-      color: $bg_danger;
-      border-color: $border_danger;
+      color: $gl-danger;
+      border-color: $gl-danger;
       background-color: #FAF1F1;
     }
   }
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
new file mode 100644
index 0000000000..15e3948e40
--- /dev/null
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -0,0 +1,9 @@
+.issues-sortable-list .str-truncated {
+  max-width: 90%;
+}
+
+li.milestone {
+  h4 {
+    font-weight: bold;
+  }
+}
diff --git a/app/assets/stylesheets/sections/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
similarity index 100%
rename from app/assets/stylesheets/sections/note_form.scss
rename to app/assets/stylesheets/pages/note_form.scss
diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/pages/notes.scss
similarity index 93%
rename from app/assets/stylesheets/sections/notes.scss
rename to app/assets/stylesheets/pages/notes.scss
index 40adc8b3ba..384ff6d740 100644
--- a/app/assets/stylesheets/sections/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -38,13 +38,11 @@ ul.notes {
     .author {
       color: #333;
       font-weight: bold;
-      font-size: 14px;
       &:hover {
-        color: $link_color;
+        color: $gl-link-color;
       }
     }
     .author-username {
-      font-size: 14px;
     }
   }
 
@@ -57,9 +55,6 @@ ul.notes {
   .note {
     display: block;
     position:relative;
-    .attachment {
-      font-size: 14px;
-    }
     .note-body {
       overflow: auto;
       .note-text {
@@ -70,7 +65,7 @@ ul.notes {
         a[href*="/uploads/"] {
           &:before {
             margin-right: 4px;
-            
+
             font: normal normal normal 14px/1 FontAwesome;
             font-size: inherit;
             text-rendering: auto;
@@ -153,7 +148,6 @@ ul.notes {
       @extend .cgray;
 
       &:hover {
-        color: $link_hover_color;
         &.danger { @extend .cred; }
       }
     }
@@ -181,10 +175,11 @@ ul.notes {
     background: #FFF;
     padding: 4px;
     font-size: 16px;
-    color: $link_color;
+    color: $gl-link-color;
     margin-left: -60px;
     position: absolute;
     z-index: 10;
+    width: 32px;
 
     transition: all 0.2s ease;
 
@@ -193,8 +188,9 @@ ul.notes {
     filter: alpha(opacity=0);
 
     &:hover {
-      font-size: 24px;
-      background: $bg_primary;
+      width: 38px;
+      font-size: 20px;
+      background: $gl-info;
       color: #FFF;
       @include show-add-diff-note;
     }
diff --git a/app/assets/stylesheets/sections/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
similarity index 75%
rename from app/assets/stylesheets/sections/notifications.scss
rename to app/assets/stylesheets/pages/notifications.scss
index f11c5dff4a..cc273f5522 100644
--- a/app/assets/stylesheets/sections/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -10,13 +10,13 @@
 }
 
 .ns-part {
-  color: $bg_primary;
+  color: $gl-primary;
 }
 
 .ns-watch {
-  color: $bg_success;
+  color: $gl-success;
 }
 
 .ns-mute {
-  color: $bg_danger;
+  color: $gl-danger;
 }
diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/pages/profile.scss
similarity index 100%
rename from app/assets/stylesheets/sections/profile.scss
rename to app/assets/stylesheets/pages/profile.scss
diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/pages/projects.scss
similarity index 91%
rename from app/assets/stylesheets/sections/projects.scss
rename to app/assets/stylesheets/pages/projects.scss
index 8bad9b139f..bfd05973d7 100644
--- a/app/assets/stylesheets/sections/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -99,25 +99,6 @@
     margin-right: 45px;
   }
 
-  .btn,
-  .form-control {
-    border: 1px solid #E1E1E1;
-    box-shadow: none;
-    padding: 6px 9px;
-  }
-
-  .btn {
-    background: none;
-    color: $link_color;
-
-    &.active {
-      background-color: #f5f5f5;
-      border: 1px solid rgba(0,0,0,0.195);
-      color: #333;
-      font-weight: bold;
-    }
-  }
-
   .form-control {
     cursor: auto;
     @extend .monospace;
@@ -267,15 +248,15 @@ ul.nav.nav-projects-tabs {
 }
 
 .vs-public {
-  color: $bg_primary;
+  color: $gl-primary;
 }
 
 .vs-internal {
-  color: $bg_warning;
+  color: $gl-warning;
 }
 
 .vs-private {
-  color: $bg_success;
+  color: $gl-success;
 }
 
 .breadcrumb.repo-breadcrumb {
diff --git a/app/assets/stylesheets/sections/search.scss b/app/assets/stylesheets/pages/search.scss
similarity index 100%
rename from app/assets/stylesheets/sections/search.scss
rename to app/assets/stylesheets/pages/search.scss
diff --git a/app/assets/stylesheets/sections/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
similarity index 100%
rename from app/assets/stylesheets/sections/snippets.scss
rename to app/assets/stylesheets/pages/snippets.scss
diff --git a/app/assets/stylesheets/sections/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
similarity index 100%
rename from app/assets/stylesheets/sections/stat_graph.scss
rename to app/assets/stylesheets/pages/stat_graph.scss
diff --git a/app/assets/stylesheets/sections/themes.scss b/app/assets/stylesheets/pages/themes.scss
similarity index 100%
rename from app/assets/stylesheets/sections/themes.scss
rename to app/assets/stylesheets/pages/themes.scss
diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/pages/tree.scss
similarity index 94%
rename from app/assets/stylesheets/sections/tree.scss
rename to app/assets/stylesheets/pages/tree.scss
index 60a1c00b04..ce02cdb165 100644
--- a/app/assets/stylesheets/sections/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -39,14 +39,9 @@
     .tree-item-file-name {
       max-width: 320px;
       vertical-align: middle;
-      a {
-        &:hover {
-          color: $link_hover_color;
-        }
-      }
 
-      i {
-        color: $bg_primary;
+      i, a {
+        color: $gl-link-color;
       }
 
       img {
@@ -66,13 +61,18 @@
 
   .tree_author {
     padding-right: 8px;
+
+    .commit-author-name {
+      color: gray;
+    }
   }
 
   .tree_commit {
     color: gray;
 
     .tree-commit-link {
-      color: #444;
+      color: gray;
+
       &:hover {
         text-decoration: underline;
       }
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
new file mode 100644
index 0000000000..277afa1db9
--- /dev/null
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -0,0 +1,9 @@
+.gitlab-ui-dev-kit {
+  > h2 {
+    font-size: 27px;
+    border-bottom: 1px solid #CCC;
+    color: #666;
+    margin: 30px 0;
+    font-weight: bold;
+  }
+}
diff --git a/app/assets/stylesheets/sections/votes.scss b/app/assets/stylesheets/pages/votes.scss
similarity index 100%
rename from app/assets/stylesheets/sections/votes.scss
rename to app/assets/stylesheets/pages/votes.scss
diff --git a/app/assets/stylesheets/sections/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
similarity index 100%
rename from app/assets/stylesheets/sections/wiki.scss
rename to app/assets/stylesheets/pages/wiki.scss
diff --git a/app/assets/stylesheets/sections/markdown_area.scss b/app/assets/stylesheets/sections/markdown_area.scss
deleted file mode 100644
index 8ee8eaa4ee..0000000000
--- a/app/assets/stylesheets/sections/markdown_area.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.markdown-area {
-  background: #FFF;
-  border: 1px solid #ddd;
-  min-height: 100px;
-  padding: 5px;
-  font-size: 14px;
-  box-shadow: none;
-  width: 100%;
-}
diff --git a/app/assets/stylesheets/sections/milestone.scss b/app/assets/stylesheets/sections/milestone.scss
deleted file mode 100644
index d20391e38f..0000000000
--- a/app/assets/stylesheets/sections/milestone.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.issues-sortable-list .str-truncated {
-  max-width: 70%;
-}
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index e80cabd6e1..44a3f1379d 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -45,7 +45,8 @@ class Admin::ServicesController < Admin::ApplicationController
         :room, :recipients, :project_url, :webhook,
         :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
         :build_key, :server, :teamcity_url, :build_type,
-        :description, :issues_url, :new_issue_url, :restrict_to_branch
+        :description, :issues_url, :new_issue_url, :restrict_to_branch,
+        :send_from_committer_email, :disable_diffs
     ])
   end
 end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index ecedb31a7f..693970e534 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -24,7 +24,7 @@ class Admin::UsersController < Admin::ApplicationController
 
   def block
     if user.block
-      redirect_to :back, alert: "Successfully blocked"
+      redirect_to :back, notice: "Successfully blocked"
     else
       redirect_to :back, alert: "Error occurred. User was not blocked"
     end
@@ -32,7 +32,7 @@ class Admin::UsersController < Admin::ApplicationController
 
   def unblock
     if user.activate
-      redirect_to :back, alert: "Successfully unblocked"
+      redirect_to :back, notice: "Successfully unblocked"
     else
       redirect_to :back, alert: "Error occurred. User was not unblocked"
     end
diff --git a/app/controllers/profiles/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
similarity index 71%
rename from app/controllers/profiles/groups_controller.rb
rename to app/controllers/dashboard/groups_controller.rb
index ce9dd50df6..61d691e636 100644
--- a/app/controllers/profiles/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,6 +1,4 @@
-class Profiles::GroupsController < ApplicationController
-  layout "profile"
-
+class Dashboard::GroupsController < ApplicationController
   def index
     @user_groups = current_user.group_members.page(params[:page]).per(20)
   end
@@ -9,7 +7,7 @@ class Profiles::GroupsController < ApplicationController
     @users_group = group.group_members.where(user_id: current_user.id).first
     if can?(current_user, :destroy, @users_group)
       @users_group.destroy
-      redirect_to(profile_groups_path, info: "You left #{group.name} group.")
+      redirect_to(dashboard_groups_path, info: "You left #{group.name} group.")
     else
       return render_403
     end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
new file mode 100644
index 0000000000..386e283f3a
--- /dev/null
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -0,0 +1,34 @@
+class Dashboard::MilestonesController < ApplicationController
+  before_filter :load_projects
+
+  def index
+    project_milestones = case params[:state]
+                         when 'all'; state
+                         when 'closed'; state('closed')
+                         else state('active')
+                         end
+    @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
+    @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(30)
+  end
+
+  def show
+    project_milestones = Milestone.where(project_id: @projects).order("due_date ASC")
+    @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
+  end
+
+  private
+
+  def load_projects
+    @projects = current_user.authorized_projects.sorted_by_activity.non_archived
+  end
+
+  def title
+    params[:title]
+  end
+
+  def state(state = nil)
+    conditions = { project_id: @projects }
+    conditions.reverse_merge!(state: state) if state
+    Milestone.where(conditions).order("title ASC")
+  end
+end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
new file mode 100644
index 0000000000..56e6fcc41c
--- /dev/null
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -0,0 +1,27 @@
+class Dashboard::ProjectsController < ApplicationController
+  before_filter :event_filter
+
+  def starred
+    @projects = current_user.starred_projects
+    @projects = @projects.includes(:namespace, :forked_from_project, :tags)
+    @projects = @projects.sort(@sort = params[:sort])
+    @groups = []
+
+    respond_to do |format|
+      format.html
+
+      format.json do
+        load_events
+        pager_json("events/_events", @events.count)
+      end
+    end
+  end
+
+  private
+
+  def load_events
+    @events = Event.in_projects(@projects.pluck(:id))
+    @events = @event_filter.apply_filter(@events).with_associations
+    @events = @events.limit(20).offset(params[:offset] || 0)
+  end
+end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4930029e16..0500619909 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -5,19 +5,9 @@ class DashboardController < ApplicationController
   before_filter :event_filter, only: :show
 
   def show
-    # Fetch only 30 projects.
-    # If user needs more - point to Dashboard#projects page
-    @projects_limit = 30
-
-    @groups = current_user.authorized_groups.order_name_asc
-    @has_authorized_projects = @projects.count > 0
-    @projects_count = @projects.count
-    @projects = @projects.includes(:namespace).limit(@projects_limit)
-
+    @projects = @projects.includes(:namespace)
     @last_push = current_user.recent_push
 
-    @publicish_project_count = Project.publicish(current_user).count
-
     respond_to do |format|
       format.html
 
@@ -33,29 +23,6 @@ class DashboardController < ApplicationController
     end
   end
 
-  def projects
-    @projects = case params[:scope]
-                when 'personal' then
-                  current_user.namespace.projects
-                when 'joined' then
-                  current_user.authorized_projects.joined(current_user)
-                when 'owned' then
-                  current_user.owned_projects
-                else
-                  current_user.authorized_projects
-                end
-
-    @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present?
-    @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
-    @projects = @projects.includes(:namespace, :forked_from_project, :tags)
-    @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
-    @projects = @projects.sort(@sort = params[:sort])
-    @projects = @projects.page(params[:page]).per(30)
-
-    @tags = current_user.authorized_projects.tags_on(:tags)
-    @groups = current_user.authorized_groups
-  end
-
   def merge_requests
     @merge_requests = get_merge_requests_collection
     @merge_requests = @merge_requests.page(params[:page]).per(20)
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 0e5891ae80..d664624fa6 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -6,6 +6,9 @@ class Explore::ProjectsController < ApplicationController
 
   def index
     @projects = ProjectsFinder.new.execute(current_user)
+    @tags = @projects.tags_on(:tags)
+    @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
+    @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
     @projects = @projects.search(params[:search]) if params[:search].present?
     @projects = @projects.sort(@sort = params[:sort])
     @projects = @projects.includes(:namespace).page(params[:page]).per(20)
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 860d8e0392..6802e529b5 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -4,10 +4,10 @@ class Groups::MilestonesController < ApplicationController
   before_filter :authorize_group_milestone!, only: :update
 
   def index
-    project_milestones = case params[:status]
-                         when 'all'; status
-                         when 'closed'; status('closed')
-                         else status('active')
+    project_milestones = case params[:state]
+                         when 'all'; state
+                         when 'closed'; state('closed')
+                         else state('active')
                          end
     @group_milestones = Milestones::GroupService.new(project_milestones).execute
     @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(30)
@@ -44,7 +44,7 @@ class Groups::MilestonesController < ApplicationController
     params[:title]
   end
 
-  def status(state = nil)
+  def state(state = nil)
     conditions = { project_id: group.projects }
     conditions.reverse_merge!(state: state) if state
     Milestone.where(conditions).order("title ASC")
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 0e1b0b6bd2..968823ac81 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -19,4 +19,7 @@ class HelpController < ApplicationController
 
   def shortcuts
   end
+
+  def ui
+  end
 end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 7dc0cac8d4..edb8bd4160 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -3,19 +3,17 @@ class Import::BaseController < ApplicationController
   private
 
   def get_or_create_namespace
-    existing_namespace = Namespace.find_by_path_or_name(@target_namespace)
-
-    if existing_namespace
-      if existing_namespace.owner == current_user
-        namespace = existing_namespace
-      else
+    begin
+      namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user)
+      namespace.add_owner(current_user)
+    rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
+      namespace = Namespace.find_by_path_or_name(@target_namespace)
+      unless namespace.owner == current_user
         @already_been_taken = true
         return false
       end
-    else
-      namespace = Group.create(name: @target_namespace, path: @target_namespace, owner: current_user)
-      namespace.add_owner(current_user)
-      namespace
     end
+
+    namespace
   end
 end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 79d9910ce8..b64491b466 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -37,7 +37,7 @@ class Projects::ImportsController < Projects::ApplicationController
   private
 
   def require_no_repo
-    if @project.repository_exists?
+    if @project.repository_exists? && !@project.import_in_progress?
       redirect_to(namespace_project_path(@project.namespace, @project)) and return
     end
   end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 6a2af08a19..1f1a9b4d43 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -93,7 +93,7 @@ class Projects::IssuesController < Projects::ApplicationController
   end
 
   def bulk_update
-    result = Issues::BulkUpdateService.new(project, current_user, params).execute
+    result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
     redirect_to :back, notice: "#{result[:count]} issues updated"
   end
 
@@ -141,4 +141,13 @@ class Projects::IssuesController < Projects::ApplicationController
       :milestone_id, :state_event, :task_num, label_ids: []
     )
   end
+
+  def bulk_update_params
+    params.require(:update).permit(
+      :issues_ids,
+      :assignee_id,
+      :milestone_id,
+      :state_event
+    )
+  end
 end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 26d4c51773..57c017e799 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -78,10 +78,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
 
     if @merge_request.valid?
-      redirect_to(
-        merge_request_path(@merge_request),
-        notice: 'Merge request was successfully created.'
-      )
+      redirect_to(merge_request_path(@merge_request))
     else
       @source_project = @merge_request.source_project
       @target_project = @merge_request.target_project
@@ -97,8 +94,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
         format.js
         format.html do
           redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
-                       @merge_request.target_project, @merge_request],
-                      notice: 'Merge request was successfully updated.')
+                       @merge_request.target_project, @merge_request])
+        end
+        format.json do
+          render json: {
+            saved: @merge_request.valid?,
+            assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
+          }
         end
       end
     else
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 82aad329c1..570447c746 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -51,7 +51,9 @@ class Projects::ServicesController < Projects::ApplicationController
       :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
       :build_key, :server, :teamcity_url, :build_type,
       :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
-      :colorize_messages, :channels
+      :colorize_messages, :channels,
+      :push_events, :issues_events, :merge_requests_events, :tag_push_events,
+      :note_events, :send_from_committer_email, :disable_diffs
     )
   end
 end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 08c7ce3f37..03fface2d2 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
     tag = @repository.find_tag(params[:id])
 
     if tag && @repository.rm_tag(tag.name)
-      EventCreateService.new.push_ref(@project, current_user, tag, 'rm', 'refs/tags')
+      EventCreateService.new.push_ref(@project, current_user, tag, 'rm', Gitlab::Git::TAG_REF_PREFIX)
     end
 
     respond_to do |format|
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 95f3ba0294..9b1c33d6c7 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -105,7 +105,7 @@ class ProjectsController < ApplicationController
         if request.referer.include?('/admin')
           redirect_to admin_namespaces_projects_path
         else
-          redirect_to projects_dashboard_path
+          redirect_to dashboard_path
         end
       end
     end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 5fb763aab9..938d3db286 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -1,24 +1,15 @@
 class UploadsController < ApplicationController
-  skip_before_filter :authenticate_user!, :reject_blocked!
-  before_filter :authorize_access
+  skip_before_filter :authenticate_user!
+  before_filter :find_model, :authorize_access!
 
   def show
-    unless upload_model && upload_mount
-      return not_found!
-    end
-
-    model = upload_model.find(params[:id])
-    uploader = model.send(upload_mount)
-
-    if model.respond_to?(:project) && !can?(current_user, :read_project, model.project)
-      return not_found!
-    end
+    uploader = @model.send(upload_mount)
 
     unless uploader.file_storage?
       return redirect_to uploader.url
     end
 
-    unless uploader.file.exists?
+    unless uploader.file && uploader.file.exists?
       return not_found!
     end
 
@@ -28,9 +19,34 @@ class UploadsController < ApplicationController
 
   private
 
-  def authorize_access
-    unless %w(avatar logo light_logo).include?(params[:mounted_as])
-      authenticate_user! && reject_blocked!
+  def find_model
+    unless upload_model && upload_mount
+      return not_found!
+    end
+
+    @model = upload_model.find(params[:id])
+  end
+
+  def authorize_access!
+    authorized =
+      case @model
+      when Project
+        can?(current_user, :read_project, @model)
+      when Group
+        can?(current_user, :read_group, @model)
+      when Note
+        can?(current_user, :read_project, @model.project)
+      else
+        # No authentication required for user avatars.
+        true
+      end
+
+    return if authorized
+
+    if current_user
+      not_found!
+    else
+      authenticate_user!
     end
   end
 
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 4dae96644c..c25b54eadc 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -1,20 +1,4 @@
 module DashboardHelper
-  def projects_dashboard_filter_path(options={})
-    exist_opts = {
-      sort: params[:sort],
-      scope: params[:scope],
-      group: params[:group],
-      tag: params[:tag],
-      visibility_level: params[:visibility_level],
-    }
-
-    options = exist_opts.merge(options)
-
-    path = request.path
-    path << "?#{options.to_param}"
-    path
-  end
-
   def assigned_issues_dashboard_path
     issues_dashboard_path(assignee_id: current_user.id)
   end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index d38b546e1b..779cebc013 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -166,7 +166,7 @@ module EventsHelper
 
   def event_note(text)
     text = first_line_in_markdown(text, 150)
-    sanitize(text, tags: %w(a img b pre code p))
+    sanitize(text, tags: %w(a img b pre code p span))
   end
 
   def event_commit_title(message)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
new file mode 100644
index 0000000000..7616fe6bad
--- /dev/null
+++ b/app/helpers/explore_helper.rb
@@ -0,0 +1,17 @@
+module ExploreHelper
+  def explore_projects_filter_path(options={})
+    exist_opts = {
+      sort: params[:sort],
+      scope: params[:scope],
+      group: params[:group],
+      tag: params[:tag],
+      visibility_level: params[:visibility_level],
+    }
+
+    options = exist_opts.merge(options)
+
+    path = request.path
+    path << "?#{options.to_param}"
+    path
+  end
+end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index ab30f498c0..daaefe90f1 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -31,7 +31,9 @@ module GitlabMarkdownHelper
   def markdown(text, options={})
     unless (@markdown and options == @options)
       @options = options
-      gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, {
+      gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self,
+                                                          user_color_scheme_class,
+                                                          {
                             # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch-
                             filter_html: true,
                             with_toc_data: true,
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index ac37f909ce..8518a47a3a 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -44,4 +44,8 @@ module GitlabRoutingHelper
   def merge_request_url(entity, *args)
     namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
   end
+
+  def snippet_url(entity, *args)
+    namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
+  end
 end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 47fa147dcc..59fdc0d49c 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -4,6 +4,19 @@ module MilestonesHelper
       namespace_project_milestones_path(@project.namespace, @project, opts)
     elsif @group
       group_milestones_path(@group, opts)
+    else
+      dashboard_milestones_path(opts)
+    end
+  end
+
+  def milestone_progress_bar(milestone)
+    options = {
+      class: 'progress-bar progress-bar-success',
+      style: "width: #{milestone.percent_complete}%;"
+    }
+
+    content_tag :div, class: 'progress' do
+      content_tag :div, nil, options
     end
   end
 end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 4bc40b35f2..b55129de29 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -16,28 +16,38 @@ module Emails
            subject: subject("Project was moved"))
     end
 
-    def repository_push_email(project_id, recipient, author_id, branch, compare)
+    def repository_push_email(project_id, recipient, author_id, branch, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false)
       @project = Project.find(project_id)
       @author  = User.find(author_id)
+      @reverse_compare = reverse_compare
       @compare = compare
       @commits = Commit.decorate(compare.commits)
       @diffs   = compare.diffs
-      @branch  = branch
+      @branch  = Gitlab::Git.ref_name(branch)
+      @disable_diffs = disable_diffs
+
+      @subject = "[#{@project.path_with_namespace}][#{@branch}] "
+
       if @commits.length > 1
         @target_url = namespace_project_compare_url(@project.namespace,
                                                     @project,
-                                                    from: @commits.first,
-                                                    to: @commits.last)
-        @subject = "#{@commits.length} new commits pushed to repository"
+                                                    from: Commit.new(@compare.base),
+                                                    to:   Commit.new(@compare.head))
+        @subject << "Deleted " if @reverse_compare
+        @subject << "#{@commits.length} commits: #{@commits.first.title}"
       else
         @target_url = namespace_project_commit_url(@project.namespace,
                                                    @project, @commits.first)
-        @subject = @commits.first.title
+
+        @subject << "Deleted 1 commit: " if @reverse_compare
+        @subject << @commits.first.title
       end
 
-      mail(from: sender(author_id),
+      @disable_footer = true
+
+      mail(from: sender(author_id, send_from_committer_email),
            to: recipient,
-           subject: subject(@subject))
+           subject: @subject)
     end
   end
 end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index cd2da68262..b212ea1dd2 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -35,21 +35,41 @@ class Notify < ActionMailer::Base
     )
   end
 
+  # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com",
+  # "corp.company.com" and "company.com".
+  # Respects set tld length so "company.co.uk" won't match "somethingelse.uk"
+  def self.allowed_email_domains
+    domain_parts = Gitlab.config.gitlab.host.split(".")
+    allowed_domains = []
+    begin
+      allowed_domains << domain_parts.join(".")
+      domain_parts.shift
+    end while domain_parts.length > ActionDispatch::Http::URL.tld_length
+
+    allowed_domains
+  end
+
   private
 
   # The default email address to send emails from
   def default_sender_address
     address = Mail::Address.new(Gitlab.config.gitlab.email_from)
-    address.display_name = "GitLab"
+    address.display_name = Gitlab.config.gitlab.email_display_name
     address
   end
 
   # Return an email address that displays the name of the sender.
   # Only the displayed name changes; the actual email address is always the same.
-  def sender(sender_id)
+  def sender(sender_id, send_from_user_email = false)
     if sender = User.find(sender_id)
       address = default_sender_address
       address.display_name = sender.name
+
+      sender_domain = sender.email.split("@").last
+      if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain)
+        address.address = sender.email
+      end
+
       address.format
     end
   end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 87882ef0ea..676111053d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,8 @@
 #  created_at             :datetime
 #  updated_at             :datetime
 #  home_page_url          :string(255)
+#  default_branch_protection :integer          default(2)
+#  twitter_sharing_enabled   :boolean          default(TRUE)
 #  help_text              :text
 #
 
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index f75b6e5e64..c9eeec731f 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -100,5 +100,4 @@ module Mentionable
     preexisting = references(p, original)
     create_cross_references!(p, a, preexisting)
   end
-
 end
diff --git a/app/models/event.rb b/app/models/event.rb
index 5579ab1dbb..8d20d7ef25 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -190,19 +190,19 @@ class Event < ActiveRecord::Base
   end
 
   def tag?
-    data[:ref]["refs/tags"]
+    Gitlab::Git.tag_ref?(data[:ref])
   end
 
   def branch?
-    data[:ref]["refs/heads"]
+    Gitlab::Git.branch_ref?(data[:ref])
   end
 
   def new_ref?
-    commit_from =~ /^00000/
+    Gitlab::Git.blank_ref?(commit_from)
   end
 
   def rm_ref?
-    commit_to =~ /^00000/
+    Gitlab::Git.blank_ref?(commit_to)
   end
 
   def md_ref?
@@ -226,11 +226,11 @@ class Event < ActiveRecord::Base
   end
 
   def branch_name
-    @branch_name ||= data[:ref].gsub("refs/heads/", "")
+    @branch_name ||= Gitlab::Git.ref_name(data[:ref])
   end
 
   def tag_name
-    @tag_name ||= data[:ref].gsub("refs/tags/", "")
+    @tag_name ||= Gitlab::Git.ref_name(data[:ref])
   end
 
   # Max 20 commits from push DESC
diff --git a/app/models/identity.rb b/app/models/identity.rb
index b2c3792d1c..440fcd0d05 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -6,6 +6,8 @@
 #  extern_uid :string(255)
 #  provider   :string(255)
 #  user_id    :integer
+#  created_at :datetime
+#  updated_at :datetime
 #
 
 class Identity < ActiveRecord::Base
diff --git a/app/models/note.rb b/app/models/note.rb
index 89beebab96..be3c8a5dd0 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -156,18 +156,41 @@ class Note < ActiveRecord::Base
       )
     end
 
-    def create_new_commits_note(noteable, project, author, commits)
-      commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit')
+    def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [])
+      total_count = new_commits.length + existing_commits.length
+      commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit')
       body = "Added #{commits_text}:\n\n"
 
-      commits.each do |commit|
+      if existing_commits.length > 0
+        commit_ids =
+          if existing_commits.length == 1
+            existing_commits.first.short_id
+          else
+            "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
+          end
+
+        commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit')
+
+        branch = 
+          if merge_request.for_fork?
+            "#{merge_request.target_project_namespace}:#{merge_request.target_branch}"
+          else
+            merge_request.target_branch
+          end
+
+        message = "* #{commit_ids} - _#{commits_text} from branch `#{branch}`_"
+        body << message
+        body << "\n"
+      end
+
+      new_commits.each do |commit|
         message = "* #{commit.short_id} - #{commit.title}"
         body << message
         body << "\n"
       end
 
       create(
-        noteable: noteable,
+        noteable: merge_request,
         project: project,
         author: author,
         note: body,
@@ -313,6 +336,10 @@ class Note < ActiveRecord::Base
     end
   end
 
+  def hook_attrs
+    attributes
+  end
+
   def set_diff
     # First lets find notes with same diff
     # before iterating over all mr diffs
@@ -471,6 +498,10 @@ class Note < ActiveRecord::Base
     for_merge_request? && for_diff_line?
   end
 
+  def for_project_snippet?
+    noteable_type == "Snippet"
+  end
+
   # override to return commits, which are not active record
   def noteable
     if for_commit?
diff --git a/app/models/project.rb b/app/models/project.rb
index 573c3384bf..233ec048dc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -493,8 +493,9 @@ class Project < ActiveRecord::Base
     end
   end
 
-  def execute_services(data)
-    services.select(&:active).each do |service|
+  def execute_services(data, hooks_scope = :push_hooks)
+    # Call only service hooks that are active for this scope
+    services.send(hooks_scope).each do |service|
       service.async_execute(data)
     end
   end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 66b72572b9..d52214cdd6 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -2,17 +2,21 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
 #
-
 require 'asana'
 
 class AsanaService < Service
@@ -61,13 +65,19 @@ automatically inspected. Leave blank to include all branches.'
     ]
   end
 
-  def execute(push)
+  def supported_events
+    %w(push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     Asana.configure do |client|
       client.api_key = api_key
     end
 
-    user = push[:user_name]
-    branch = push[:ref].gsub('refs/heads/', '')
+    user = data[:user_name]
+    branch = Gitlab::Git.ref_name(data[:ref])
 
     branch_restriction = restrict_to_branch.to_s
 
@@ -79,7 +89,7 @@ automatically inspected. Leave blank to include all branches.'
     project_name = project.name_with_namespace
     push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
 
-    push[:commits].each do |commit|
+    data[:commits].each do |commit|
       check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
     end
   end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index cf7598f35e..fb7e0c0fb0 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -2,15 +2,20 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
 #
 
 class AssemblaService < Service
@@ -38,8 +43,14 @@ class AssemblaService < Service
     ]
   end
 
-  def execute(push)
+  def supported_events
+    %w(push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
-    AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' })
+    AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
   end
 end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index df68803152..0100f1e4a1 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -2,15 +2,20 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
 #
 
 class BambooService < CiService
@@ -69,6 +74,10 @@ class BambooService < CiService
     ]
   end
 
+  def supported_events
+    %w(push)
+  end
+  
   def build_info(sha)
     url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
 
@@ -118,7 +127,9 @@ class BambooService < CiService
     end
   end
 
-  def execute(_data)
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     # Bamboo requires a GET and does not take any data.
     self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}",
                    verify: false)
diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb
index 058c890ae4..270863c157 100644
--- a/app/models/project_services/buildbox_service.rb
+++ b/app/models/project_services/buildbox_service.rb
@@ -2,16 +2,22 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
 #
+
 require "addressable/uri"
 
 class BuildboxService < CiService
@@ -32,7 +38,13 @@ class BuildboxService < CiService
     hook.save
   end
 
+  def supported_events
+    %w(push)
+  end
+
   def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     service_hook.execute(data)
   end
 
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 14b6b87a0b..e591afdda6 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -2,15 +2,20 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
 #
 
 class CampfireService < Service
@@ -37,11 +42,17 @@ class CampfireService < Service
     ]
   end
 
-  def execute(push_data)
+  def supported_events
+    %w(push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     room = gate.find_room_by_name(self.room)
     return true unless room
 
-    message = build_message(push_data)
+    message = build_message(data)
 
     room.speak(message)
   end
@@ -53,7 +64,7 @@ class CampfireService < Service
   end
 
   def build_message(push)
-    ref = push[:ref].gsub("refs/heads/", "")
+    ref = Gitlab::Git.ref_name(push[:ref])
     before = push[:before]
     after = push[:after]
 
@@ -61,9 +72,9 @@ class CampfireService < Service
     message << "[#{project.name_with_namespace}] "
     message << "#{push[:user_name]} "
 
-    if before.include?('000000')
+    if Gitlab::Git.blank_ref?(before)
       message << "pushed new branch #{ref} \n"
-    elsif after.include?('000000')
+    elsif Gitlab::Git.blank_ref?(after)
       message << "removed branch #{ref} \n"
     else
       message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 5a26c25b3c..c6f6b4952c 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -2,15 +2,19 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 # Base class for CI services
@@ -21,6 +25,10 @@ class CiService < Service
     :ci
   end
 
+  def supported_events
+    %w(push)
+  end
+
   # Return complete url to build page
   #
   # Ex.
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index b29d1c8688..8d25f62787 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -2,15 +2,19 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 class CustomIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 86693ad0c7..acb5e7f1af 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -2,18 +2,24 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 class EmailsOnPushService < Service
+  prop_accessor :send_from_committer_email
+  prop_accessor :disable_diffs
   prop_accessor :recipients
   validates :recipients, presence: true, if: :activated?
 
@@ -29,12 +35,31 @@ class EmailsOnPushService < Service
     'emails_on_push'
   end
 
+  def supported_events
+    %w(push)
+  end
+
   def execute(push_data)
-    EmailsOnPushWorker.perform_async(project_id, recipients, push_data)
+    return unless supported_events.include?(push_data[:object_kind])
+
+    EmailsOnPushWorker.perform_async(project_id, recipients, push_data, send_from_committer_email?, disable_diffs?)
+  end
+
+  def send_from_committer_email?
+    self.send_from_committer_email == "1"
+  end
+
+  def disable_diffs?
+    self.disable_diffs == "1"
   end
 
   def fields
+    domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
     [
+      { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer",
+        help: "Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. #{domains})." },
+      { type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs",
+        help: "Don't include possibly sensitive code diffs in notification body." },
       { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' },
     ]
   end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 13e2dfceb1..99e361dd6e 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -2,15 +2,19 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 require "flowdock-git-hook"
@@ -37,11 +41,17 @@ class FlowdockService < Service
     ]
   end
 
-  def execute(push_data)
+  def supported_events
+    %w(push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     Flowdock::Git.post(
-      push_data[:ref],
-      push_data[:before],
-      push_data[:after],
+      data[:ref],
+      data[:before],
+      data[:after],
       token: token,
       repo: project.repository.path_to_repo,
       repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}",
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index a2c87ae88f..4e75bdfc95 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -2,15 +2,19 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 require "gemnasium/gitlab_service"
@@ -38,11 +42,17 @@ class GemnasiumService < Service
     ]
   end
 
-  def execute(push_data)
+  def supported_events
+    %w(push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
     Gemnasium::GitlabService.execute(
-      ref: push_data[:ref],
-      before: push_data[:before],
-      after: push_data[:after],
+      ref: data[:ref],
+      before: data[:before],
+      after: data[:after],
       token: token,
       api_key: api_key,
       repo: project.repository.path_to_repo
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index f4b463e819..d81623625c 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -2,15 +2,19 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 class GitlabCiService < CiService
@@ -18,8 +22,6 @@ class GitlabCiService < CiService
   validates :project_url, presence: true, if: :activated?
   validates :token, presence: true, if: :activated?
 
-  delegate :execute, to: :service_hook, prefix: nil
-
   after_save :compose_service_hook, if: :activated?
 
   def compose_service_hook
@@ -28,6 +30,16 @@ class GitlabCiService < CiService
     hook.save
   end
 
+  def supported_events
+    %w(push tag_push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
+    service_hook.execute(data)
+  end
+
   def commit_status_path(sha)
     project_url + "/commits/#{sha}/status.json?token=#{token}"
   end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 05c048e4e4..84346350a6 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -2,15 +2,20 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
+#  note_events           :boolean          default(TRUE), not null
 #
 
 class GitlabIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 003e06a4c8..d264a56ebd 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -2,15 +2,19 @@
 #
 # Table name: services
 #
-#  id         :integer          not null, primary key
-#  type       :string(255)
-#  title      :string(255)
-#  project_id :integer
-#  created_at :datetime
-#  updated_at :datetime
-#  active     :boolean          default(FALSE), not null
-#  properties :text
-#  template   :boolean          default(FALSE)
+#  id                    :integer          not null, primary key
+#  type                  :string(255)
+#  title                 :string(255)
+#  project_id            :integer
+#  created_at            :datetime
+#  updated_at            :datetime
+#  active                :boolean          default(FALSE), not null
+#  properties            :text
+#  template              :boolean          default(FALSE)
+#  push_events           :boolean          default(TRUE)
+#  issues_events         :boolean          default(TRUE)
+#  merge_requests_events :boolean          default(TRUE)
+#  tag_push_events       :boolean          default(TRUE)
 #
 
 class HipchatService < Service
@@ -40,8 +44,14 @@ class HipchatService < Service
     ]
   end
 
-  def execute(push_data)
-    gate[room].send('GitLab', create_message(push_data))
+  def supported_events
+    %w(push issue merge_request note tag_push)
+  end
+
+  def execute(data)
+    return unless supported_events.include?(data[:object_kind])
+
+    gate[room].send('GitLab', create_message(data))
   end
 
   private
@@ -52,22 +62,39 @@ class HipchatService < Service
     @gate ||= HipChat::Client.new(token, options)
   end
 
-  def create_message(push)
-    ref = push[:ref].gsub("refs/heads/", "")
+  def create_message(data)
+    object_kind = data[:object_kind]
+
+    message = \
+      case object_kind
+      when "push", "tag_push"
+        create_push_message(data)
+      when "issue"
+        create_issue_message(data) unless is_update?(data)
+      when "merge_request"
+        create_merge_request_message(data) unless is_update?(data)
+      when "note"
+        create_note_message(data)
+      end
+  end
+
+  def create_push_message(push)
+    ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
+    ref = Gitlab::Git.ref_name(push[:ref])
+
     before = push[:before]
     after = push[:after]
 
     message = ""
     message << "#{push[:user_name]} "
-    if before.include?('000000')
-      message << "pushed new branch #{ref}"\
-                 " to "\
-                 "#{project.name_with_namespace.gsub!(/\s/, "")}\n"
-    elsif after.include?('000000')
-      message << "removed branch #{ref} from #{project.name_with_namespace.gsub!(/\s/,'')} \n"
+    if Gitlab::Git.blank_ref?(before)
+      message << "pushed new #{ref_type} #{ref}"\
+                 " to #{project_link}\n"
+    elsif Gitlab::Git.blank_ref?(after)
+      message << "removed #{ref_type} #{ref} from #{project_name} \n"
     else
-      message << "pushed to branch #{ref} "
       message << "of #{project.name_with_namespace.gsub!(/\s/,'')} "
       message << "(Compare changes)"
@@ -83,4 +110,129 @@ class HipchatService < Service
 
     message
   end
+
+  def format_body(body)
+    if body
+      body = body.truncate(200, separator: ' ', omission: '...')
+    end
+
+    "
#{body}
" + end + + def create_issue_message(data) + user_name = data[:user][:name] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + title = obj_attr[:title] + state = obj_attr[:state] + issue_iid = obj_attr[:iid] + issue_url = obj_attr[:url] + description = obj_attr[:description] + + issue_link = "issue ##{issue_iid}" + message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" + + if description + description = format_body(description) + message << description + end + + message + end + + def create_merge_request_message(data) + user_name = data[:user][:name] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + merge_request_id = obj_attr[:iid] + source_branch = obj_attr[:source_branch] + target_branch = obj_attr[:target_branch] + state = obj_attr[:state] + description = obj_attr[:description] + title = obj_attr[:title] + + merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" + merge_request_link = "merge request ##{merge_request_id}" + message = "#{user_name} #{state} #{merge_request_link} in " \ + "#{project_link}: #{title}" + + if description + description = format_body(description) + message << description + end + + message + end + + def format_title(title) + "" + title.lines.first.chomp + "" + end + + def create_note_message(data) + data = HashWithIndifferentAccess.new(data) + user_name = data[:user][:name] + + repo_attr = HashWithIndifferentAccess.new(data[:repository]) + + obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) + note = obj_attr[:note] + note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + commit_attr = HashWithIndifferentAccess.new(data[:commit]) + subject_desc = commit_attr[:id] + subject_desc = Commit.truncate_sha(subject_desc) + subject_type = "commit" + title = format_title(commit_attr[:message]) + when "Issue" + subj_attr = HashWithIndifferentAccess.new(data[:issue]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "issue" + title = format_title(subj_attr[:title]) + when "MergeRequest" + subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "merge request" + title = format_title(subj_attr[:title]) + when "Snippet" + subj_attr = HashWithIndifferentAccess.new(data[:snippet]) + subject_id = subj_attr[:id] + subject_desc = "##{subject_id}" + subject_type = "snippet" + title = format_title(subj_attr[:title]) + end + + subject_html = "#{subject_type} #{subject_desc}" + message = "#{user_name} commented on #{subject_html} in #{project_link}: " + message << title + + if note + note = format_body(note) + message << note + end + + message + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def project_link + "#{project_name}" + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index a0203a5bb1..deb210c61e 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# require 'uri' diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index ad28e87af2..7afd16d2d7 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class IssueTrackerService < Service @@ -25,18 +30,6 @@ class IssueTrackerService < Service false end - def project_url - # implement inside child - end - - def issues_url - # implement inside child - end - - def new_issue_url - # implement inside child - end - def create_cross_reference_note # implement inside child end @@ -59,9 +52,9 @@ class IssueTrackerService < Service if enabled_in_gitlab_config self.properties = { title: issues_tracker['title'], - project_url: set_project_url, - issues_url: issues_tracker['issues_url'], - new_issue_url: issues_tracker['new_issue_url'] + project_url: add_issues_tracker_id(issues_tracker['project_url']), + issues_url: add_issues_tracker_id(issues_tracker['issues_url']), + new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url']) } else self.properties = {} @@ -69,7 +62,13 @@ class IssueTrackerService < Service end end + def supported_events + %w(push) + end + def execute(data) + return unless supported_events.include?(data[:object_kind]) + message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." result = false @@ -104,15 +103,15 @@ class IssueTrackerService < Service Gitlab.config.issues_tracker[to_param] end - def set_project_url + def add_issues_tracker_id(url) if self.project id = self.project.issues_tracker_id if id - issues_tracker['project_url'].gsub(":issues_tracker_id", id) + url = url.gsub(":issues_tracker_id", id) end end - issues_tracker['project_url'] + url end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 94bf15ef1e..e71db234ef 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class JiraService < IssueTrackerService diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 287812c57a..ade9ee9787 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class PivotaltrackerService < Service @@ -37,9 +42,15 @@ class PivotaltrackerService < Service ] end - def execute(push) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + url = 'https://www.pivotaltracker.com/services/v5/source_commits' - push[:commits].each do |commit| + data[:commits].each do |commit| message = { 'source_commit' => { 'commit_id' => commit[:id], diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3a3af59390..53edf522e9 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class PushoverService < Service @@ -76,21 +81,27 @@ class PushoverService < Service ] end - def execute(push_data) - ref = push_data[:ref].gsub('refs/heads/', '') - before = push_data[:before] - after = push_data[:after] + def supported_events + %w(push) + end - if before.include?('000000') - message = "#{push_data[:user_name]} pushed new branch \"#{ref}\"." - elsif after.include?('000000') - message = "#{push_data[:user_name]} deleted branch \"#{ref}\"." + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + ref = Gitlab::Git.ref_name(data[:ref]) + before = data[:before] + after = data[:after] + + if Gitlab::Git.blank_ref?(before) + message = "#{data[:user_name]} pushed new branch \"#{ref}\"." + elsif Gitlab::Git.blank_ref?(after) + message = "#{data[:user_name]} deleted branch \"#{ref}\"." else - message = "#{push_data[:user_name]} push to branch \"#{ref}\"." + message = "#{data[:user_name]} push to branch \"#{ref}\"." end - if push_data[:total_commits_count] > 0 - message << "\nTotal commits count: #{push_data[:total_commits_count]}" + if data[:total_commits_count] > 0 + message << "\nTotal commits count: #{data[:total_commits_count]}" end pushover_data = { @@ -100,7 +111,7 @@ class PushoverService < Service priority: priority, title: "#{project.name_with_namespace}", message: message, - url: push_data[:repository][:homepage], + url: data[:repository][:homepage], url_title: "See project #{project.name_with_namespace}" } diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index e1dc10415e..dd9ba97ee1 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class RedmineService < IssueTrackerService diff --git a/app/models/project_services/slack_message.rb b/app/models/project_services/slack_message.rb deleted file mode 100644 index 6c6446db45..0000000000 --- a/app/models/project_services/slack_message.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'slack-notifier' - -class SlackMessage - attr_reader :after - attr_reader :before - attr_reader :commits - attr_reader :project_name - attr_reader :project_url - attr_reader :ref - attr_reader :username - - def initialize(params) - @after = params.fetch(:after) - @before = params.fetch(:before) - @commits = params.fetch(:commits, []) - @project_name = params.fetch(:project_name) - @project_url = params.fetch(:project_url) - @ref = params.fetch(:ref).gsub('refs/heads/', '') - @username = params.fetch(:user_name) - end - - def pretext - format(message) - end - - def attachments - return [] if new_branch? || removed_branch? - - commit_message_attachments - end - - private - - def message - if new_branch? - new_branch_message - elsif removed_branch? - removed_branch_message - else - push_message - end - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def new_branch_message - "#{username} pushed new branch #{branch_link} to #{project_link}" - end - - def removed_branch_message - "#{username} removed branch #{ref} from #{project_link}" - end - - def push_message - "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" - end - - def commit_messages - commits.each_with_object('') do |commit, str| - str << compose_commit_message(commit) - end.chomp - end - - def commit_message_attachments - [{ text: format(commit_messages), color: attachment_color }] - end - - def compose_commit_message(commit) - author = commit.fetch(:author).fetch(:name) - id = commit.fetch(:id)[0..8] - message = commit.fetch(:message) - url = commit.fetch(:url) - - "[#{id}](#{url}): #{message} - #{author}\n" - end - - def new_branch? - before.include?('000000') - end - - def removed_branch? - after.include?('000000') - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def compare_url - "#{project_url}/compare/#{before}...#{after}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def compare_link - "[Compare changes](#{compare_url})" - end - - def attachment_color - '#345' - end -end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index c7cbff63fe..36d9874edd 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class SlackService < Service @@ -38,20 +43,45 @@ class SlackService < Service ] end - def execute(push_data) + def supported_events + %w(push issue merge_request note tag_push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) return unless webhook.present? - message = SlackMessage.new(push_data.merge( + object_kind = data[:object_kind] + + data = data.merge( project_url: project_url, project_name: project_name - )) + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = \ + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + end opt = {} opt[:channel] = channel if channel opt[:username] = username if username - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments) + if message + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments) + end end private @@ -63,4 +93,13 @@ class SlackService < Service def project_url project.web_url end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end end + +require "slack_service/issue_message" +require "slack_service/push_message" +require "slack_service/merge_message" +require "slack_service/note_message" diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb new file mode 100644 index 0000000000..aa00d6061a --- /dev/null +++ b/app/models/project_services/slack_service/base_message.rb @@ -0,0 +1,31 @@ +require 'slack-notifier' + +class SlackService + class BaseMessage + def initialize(params) + raise NotImplementedError + end + + def pretext + format(message) + end + + def attachments + raise NotImplementedError + end + + private + + def message + raise NotImplementedError + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb new file mode 100644 index 0000000000..5af24a8060 --- /dev/null +++ b/app/models/project_services/slack_service/issue_message.rb @@ -0,0 +1,56 @@ +class SlackService + class IssueMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :issue_iid + attr_reader :issue_url + attr_reader :action + attr_reader :state + attr_reader :description + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @issue_iid = obj_attr[:iid] + @issue_url = obj_attr[:url] + @action = obj_attr[:action] + @state = obj_attr[:state] + @description = obj_attr[:description] + end + + def attachments + return [] unless opened_issue? + + description_message + end + + private + + def message + "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" + end + + def opened_issue? + action == "open" + end + + def description_message + [{ text: format(description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def issue_link + "[issue ##{issue_iid}](#{issue_url})" + end + end +end diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb new file mode 100644 index 0000000000..e792c258f7 --- /dev/null +++ b/app/models/project_services/slack_service/merge_message.rb @@ -0,0 +1,60 @@ +class SlackService + class MergeMessage < BaseMessage + attr_reader :user_name + attr_reader :project_name + attr_reader :project_url + attr_reader :merge_request_id + attr_reader :source_branch + attr_reader :target_branch + attr_reader :state + attr_reader :title + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @merge_request_id = obj_attr[:iid] + @source_branch = obj_attr[:source_branch] + @target_branch = obj_attr[:target_branch] + @state = obj_attr[:state] + @title = format_title(obj_attr[:title]) + end + + def pretext + format(message) + end + + def attachments + [] + end + + private + + def format_title(title) + '*' + title.lines.first.chomp + '*' + end + + def message + merge_request_message + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def merge_request_message + "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" + end + + def merge_request_link + "[merge request ##{merge_request_id}](#{merge_request_url})" + end + + def merge_request_url + "#{project_url}/merge_requests/#{merge_request_id}" + end + end +end diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb new file mode 100644 index 0000000000..074478b292 --- /dev/null +++ b/app/models/project_services/slack_service/note_message.rb @@ -0,0 +1,82 @@ +class SlackService + class NoteMessage < BaseMessage + attr_reader :message + attr_reader :user_name + attr_reader :project_name + attr_reader :project_link + attr_reader :note + attr_reader :note_url + attr_reader :title + + def initialize(params) + params = HashWithIndifferentAccess.new(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @note = obj_attr[:note] + @note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + create_commit_note(HashWithIndifferentAccess.new(params[:commit])) + when "Issue" + create_issue_note(HashWithIndifferentAccess.new(params[:issue])) + when "MergeRequest" + create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) + when "Snippet" + create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) + end + end + + def attachments + description_message + end + + private + + def format_title(title) + title.lines.first.chomp + end + + def create_commit_note(commit) + commit_sha = commit[:id] + commit_sha = Commit.truncate_sha(commit_sha) + commit_link = "[commit #{commit_sha}](#{@note_url})" + title = format_title(commit[:message]) + @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*" + end + + def create_issue_note(issue) + issue_iid = issue[:iid] + note_link = "[issue ##{issue_iid}](#{@note_url})" + title = format_title(issue[:title]) + @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*" + end + + def create_merge_note(merge_request) + merge_request_id = merge_request[:iid] + merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" + title = format_title(merge_request[:title]) + @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*" + end + + def create_snippet_note(snippet) + snippet_id = snippet[:id] + snippet_link = "[snippet ##{snippet_id}](#{@note_url})" + title = format_title(snippet[:title]) + @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@note), color: attachment_color }] + end + + def project_link + "[#{@project_name}](#{@project_url})" + end + end +end diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb new file mode 100644 index 0000000000..b26f3e9ddc --- /dev/null +++ b/app/models/project_services/slack_service/push_message.rb @@ -0,0 +1,110 @@ +class SlackService + class PushMessage < BaseMessage + attr_reader :after + attr_reader :before + attr_reader :commits + attr_reader :project_name + attr_reader :project_url + attr_reader :ref + attr_reader :ref_type + attr_reader :user_name + + def initialize(params) + @after = params[:after] + @before = params[:before] + @commits = params.fetch(:commits, []) + @project_name = params[:project_name] + @project_url = params[:project_url] + @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' + @ref = Gitlab::Git.ref_name(params[:ref]) + @user_name = params[:user_name] + end + + def pretext + format(message) + end + + def attachments + return [] if new_branch? || removed_branch? + + commit_message_attachments + end + + private + + def message + if new_branch? + new_branch_message + elsif removed_branch? + removed_branch_message + else + push_message + end + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def new_branch_message + "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" + end + + def removed_branch_message + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" + end + + def push_message + "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" + end + + def commit_messages + commits.map { |commit| compose_commit_message(commit) }.join("\n") + end + + def commit_message_attachments + [{ text: format(commit_messages), color: attachment_color }] + end + + def compose_commit_message(commit) + author = commit[:author][:name] + id = Commit.truncate_sha(commit[:id]) + message = commit[:message] + url = commit[:url] + + "[#{id}](#{url}): #{message} - #{author}" + end + + def new_branch? + Gitlab::Git.blank_ref?(before) + end + + def removed_branch? + Gitlab::Git.blank_ref?(after) + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b6932f1c77..7403e19da9 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class TeamcityService < CiService @@ -57,6 +62,10 @@ class TeamcityService < CiService 'teamcity' end + def supported_events + %w(push) + end + def fields [ { type: 'text', name: 'teamcity_url', @@ -115,13 +124,15 @@ class TeamcityService < CiService end end - def execute(push) + def execute(data) + return unless supported_events.include?(data[:object_kind]) + auth = { username: username, password: password, } - branch = push[:ref].gsub('refs/heads/', '') + branch = Gitlab::Git.ref_name(data[:ref]) self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", body: ""\ diff --git a/app/models/repository.rb b/app/models/repository.rb index 5b52739df2..6117db418a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -146,7 +146,8 @@ class Repository end def timestamps_by_user_log(user) - args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short) + author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')' + args = %W(git log -E --author=#{author_emails} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short) dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") if dates.present? diff --git a/app/models/service.rb b/app/models/service.rb index adaff1c54f..39d1719838 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# # To add new service you should build a class inherited from Service # and implement a set of methods @@ -19,6 +24,11 @@ class Service < ActiveRecord::Base serialize :properties, JSON default_value_for :active, false + default_value_for :push_events, true + default_value_for :issues_events, true + default_value_for :merge_requests_events, true + default_value_for :tag_push_events, true + default_value_for :note_events, true after_initialize :initialize_properties @@ -29,6 +39,12 @@ class Service < ActiveRecord::Base scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } + scope :push_hooks, -> { where(push_events: true, active: true) } + scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } + scope :issue_hooks, -> { where(issues_events: true, active: true) } + scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } + scope :note_hooks, -> { where(note_events: true, active: true) } + def activated? active end @@ -66,6 +82,10 @@ class Service < ActiveRecord::Base [] end + def supported_events + %w(push tag_push issue merge_request) + end + def execute # implement inside child end @@ -91,6 +111,8 @@ class Service < ActiveRecord::Base end def async_execute(data) + return unless supported_events.include?(data[:object_kind]) + Sidekiq::Client.enqueue(ProjectServiceWorker, id, data) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 82c1ab9444..3fb2ec1d66 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -59,6 +59,10 @@ class Snippet < ActiveRecord::Base content end + def hook_attrs + attributes + end + def size 0 end diff --git a/app/models/user.rb b/app/models/user.rb index f766ea1100..a92095e582 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,51 +2,53 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# hide_no_password :boolean default(FALSE) -# website_url :string(255) default(""), not null -# last_credential_check_at :datetime -# github_access_token :string(255) -# notification_email :string(255) -# password_automatically_set :boolean default(FALSE) -# bitbucket_access_token :string(255) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# github_access_token :string(255) +# gitlab_access_token :string(255) +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# bitbucket_access_token :string(255) +# bitbucket_access_token_secret :string(255) # require 'carrierwave/orm/activerecord' @@ -152,24 +154,6 @@ class User < ActiveRecord::Base delegate :path, to: :namespace, allow_nil: true, prefix: true state_machine :state, initial: :active do - after_transition any => :blocked do |user, transition| - # Remove user from all projects and - user.project_members.find_each do |membership| - # skip owned resources - next if membership.project.owner == user - - return false unless membership.destroy - end - - # Remove user from all groups - user.group_members.find_each do |membership| - # skip owned resources - next if membership.group.last_owner?(user) - - return false unless membership.destroy - end - end - event :block do transition active: :blocked end @@ -640,7 +624,7 @@ class User < ActiveRecord::Base def contributed_projects_ids Event.where(author_id: self). where("created_at > ?", Time.now - 1.year). - where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)", + where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)", pushed: Event::PUSHED, created: Event::CREATED). reorder(project_id: :desc). select(:project_id). diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index a735d3f7f2..dfc5677c9d 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -21,12 +21,12 @@ class CreateTagService < BaseService new_tag = repository.find_tag(tag_name) if new_tag - if project.gitlab_ci? - push_data = create_push_data(project, current_user, new_tag) - project.gitlab_ci_service.async_execute(push_data) - end + EventCreateService.new.push_ref(project, current_user, new_tag, 'add', Gitlab::Git::TAG_REF_PREFIX) + + push_data = create_push_data(project, current_user, new_tag) + project.execute_hooks(push_data.dup, :tag_push_hooks) + project.execute_services(push_data.dup, :tag_push_hooks) - EventCreateService.new.push_ref(project, current_user, new_tag, 'add', 'refs/tags') success(new_tag) else error('Invalid reference name') @@ -40,7 +40,9 @@ class CreateTagService < BaseService end def create_push_data(project, user, tag) - Gitlab::PushDataBuilder. - build(project, user, Gitlab::Git::BLANK_SHA, tag.target, 'refs/tags/' + tag.name, []) + data = Gitlab::PushDataBuilder. + build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + data[:object_kind] = "tag_push" + data end end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index ba9547b924..dc52d6d89d 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -62,19 +62,19 @@ class EventCreateService create_event(project, current_user, Event::CREATED) end - def push_ref(project, current_user, ref, action = 'add', prefix = 'refs/heads') + def push_ref(project, current_user, ref, action = 'add', prefix = Gitlab::Git::BRANCH_REF_PREFIX) commit = project.repository.commit(ref.target) if action.to_s == 'add' - before = '00000000' + before = Gitlab::Git::BLANK_SHA after = commit.id else before = commit.id - after = '00000000' + after = Gitlab::Git::BLANK_SHA end data = { - ref: "#{prefix}/#{ref.name}", + ref: "#{prefix}#{ref.name}", before: before, after: after } diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index b3796d1c81..742781f631 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -42,8 +42,10 @@ class GitPushService # as a heuristic. This may include more commits than are actually pushed, but # that shouldn't matter because we check for existing cross-references later. @push_commits = project.repository.commits_between(project.default_branch, newrev) + + # don't process commits for the initial push to the default branch + process_commit_messages(ref) end - process_commit_messages(ref) elsif push_to_existing_branch?(ref, oldrev) # Collect data for this git push @push_commits = project.repository.commits_between(oldrev, newrev) @@ -54,7 +56,7 @@ class GitPushService @push_data = post_receive_data(oldrev, newrev, ref) EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) - project.execute_services(@push_data.dup) + project.execute_services(@push_data.dup, :push_hooks) end end @@ -109,30 +111,24 @@ class GitPushService end def push_to_existing_branch?(ref, oldrev) - ref_parts = ref.split('/') - # Return if this is not a push to a branch (e.g. new commits) - ref_parts[1].include?('heads') && oldrev != Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && oldrev != Gitlab::Git::BLANK_SHA end def push_to_new_branch?(ref, oldrev) - ref_parts = ref.split('/') - - ref_parts[1].include?('heads') && oldrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev) end def push_remove_branch?(ref, newrev) - ref_parts = ref.split('/') - - ref_parts[1].include?('heads') && newrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev) end def push_to_branch?(ref) - ref.include?('refs/heads') + Gitlab::Git.branch_ref?(ref) end def is_default_branch?(ref) - ref == "refs/heads/#{project.default_branch}" + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.ref_name(ref) == project.default_branch end def commit_user(commit) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 46d8987f12..cd92f50b02 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -8,10 +8,7 @@ class GitTagPushService EventCreateService.new.push(project, user, @push_data) project.repository.expire_cache project.execute_hooks(@push_data.dup, :tag_push_hooks) - - if project.gitlab_ci? - project.gitlab_ci_service.async_execute(@push_data) - end + project.execute_services(@push_data.dup, :tag_push_hooks) true end @@ -19,7 +16,8 @@ class GitTagPushService private def create_push_data(oldrev, newrev, ref) - Gitlab::PushDataBuilder. - build(project, user, oldrev, newrev, ref, []) + data = Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, []) + data[:object_kind] = "tag_push" + data end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 755c0ef45a..c3ca04a434 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,13 +1,19 @@ module Issues class BaseService < ::IssuableBaseService + def hook_data(issue, action) + issue_data = issue.to_hook_data(current_user) + issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) + issue_data[:object_attributes].merge!(url: issue_url, action: action) + issue_data + end + private def execute_hooks(issue, action = 'open') - issue_data = issue.to_hook_data(current_user) - issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) - issue_data[:object_attributes].merge!(url: issue_url, action: action) + issue_data = hook_data(issue, action) issue.project.execute_hooks(issue_data, :issue_hooks) + issue.project.execute_services(issue_data, :issue_hooks) end end end diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index f72a346af6..c7cd20b6b6 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -1,38 +1,23 @@ module Issues class BulkUpdateService < BaseService def execute - update_data = params[:update] + issues_ids = params.delete(:issues_ids).split(",") + issue_params = params - issues_ids = update_data[:issues_ids].split(",") - milestone_id = update_data[:milestone_id] - assignee_id = update_data[:assignee_id] - status = update_data[:status] - - new_state = nil - - if status.present? - if status == 'closed' - new_state = :close - else - new_state = :reopen - end - end - - opts = {} - opts[:milestone_id] = milestone_id if milestone_id.present? - opts[:assignee_id] = assignee_id if assignee_id.present? + issue_params.delete(:state_event) unless issue_params[:state_event].present? + issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? + issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? issues = Issue.where(id: issues_ids) - issues = issues.select { |issue| can?(current_user, :modify_issue, issue) } - issues.each do |issue| - issue.update_attributes(opts) - issue.send new_state if new_state + next unless can?(current_user, :modify_issue, issue) + + Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) end { - count: issues.count, - success: !issues.count.zero? + count: issues.count, + success: !issues.count.zero? } end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index b4199d1c80..f6e1ae6f28 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -5,13 +5,19 @@ module MergeRequests Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end + def hook_data(merge_request, action) + hook_data = merge_request.to_hook_data(current_user) + merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) + hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:action] = action + hook_data + end + def execute_hooks(merge_request, action = 'open') if merge_request.project - hook_data = merge_request.to_hook_data(current_user) - merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - hook_data[:object_attributes][:url] = merge_request_url - hook_data[:object_attributes][:action] = action - merge_request.project.execute_hooks(hook_data, :merge_request_hooks) + merge_data = hook_data(merge_request, action) + merge_request.project.execute_hooks(merge_data, :merge_request_hooks) + merge_request.project.execute_services(merge_data, :merge_request_hooks) end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 96761bec99..cab8a1e880 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -1,10 +1,10 @@ module MergeRequests class RefreshService < MergeRequests::BaseService def execute(oldrev, newrev, ref) - return true unless ref =~ /heads/ + return true unless Gitlab::Git.branch_ref?(ref) @oldrev, @newrev = oldrev, newrev - @branch_name = ref.gsub("refs/heads/", "") + @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened @commits = @project.repository.commits_between(oldrev, newrev) @@ -82,8 +82,14 @@ module MergeRequests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| + mr_commit_ids = Set.new(merge_request.commits.map(&:id)) + + new_commits, existing_commits = @commits.partition do |commit| + mr_commit_ids.include?(commit.id) + end + Note.create_new_commits_note(merge_request, merge_request.project, - @current_user, @commits) + @current_user, new_commits, existing_commits) end end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index f64006a4ed..e969061f22 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -17,10 +17,22 @@ module Notes note.references.each do |mentioned| Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project) end + + execute_hooks(note) end end note end + + def hook_data(note) + Gitlab::NoteDataBuilder.build(note, current_user) + end + + def execute_hooks(note) + note_data = hook_data(note) + # TODO: Support Webhooks + note.project.execute_services(note_data, :note_hooks) + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 2fc63b9f4b..0063b7ce40 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -268,6 +268,7 @@ class NotificationService # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) users = users.to_a.compact.uniq + users = users.reject(&:blocked?) users.reject do |user| next user.notification.disabled? unless project diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 46f6e91e80..c5d0b08845 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -41,7 +41,7 @@ class SystemHooksService path_with_namespace: model.path_with_namespace, project_id: model.id, owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : nil, + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase }) when User diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 13648c0d20..46c5908f1b 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -8,39 +8,47 @@ %fieldset %legend Features .form-group - = f.label :signup_enabled, class: 'control-label' - .col-sm-10 - = f.check_box :signup_enabled, class: 'checkbox' + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signup_enabled do + = f.check_box :signup_enabled + Signin enabled .form-group - = f.label :signin_enabled, class: 'control-label' - .col-sm-10 - = f.check_box :signin_enabled, class: 'checkbox' + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signin_enabled do + = f.check_box :signin_enabled + Signup enabled .form-group - = f.label :gravatar_enabled, class: 'control-label' - .col-sm-10 - = f.check_box :gravatar_enabled, class: 'checkbox' + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :gravatar_enabled do + = f.check_box :gravatar_enabled + Gravatar enabled .form-group - = f.label :twitter_sharing_enabled, "Twitter enabled", class: 'control-label' - .col-sm-10 - = f.check_box :twitter_sharing_enabled, class: 'checkbox' - %span.help-block Show users button to share their newly created public or internal projects on twitter + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :twitter_sharing_enabled do + = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' + %strong Twitter enabled + %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter %fieldset %legend Misc .form-group - = f.label :default_projects_limit, class: 'control-label' + = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 = f.number_field :default_projects_limit, class: 'form-control' .form-group - = f.label :default_branch_protection, class: 'control-label' + = f.label :default_branch_protection, class: 'control-label col-sm-2' .col-sm-10 = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' .form-group - = f.label :home_page_url, class: 'control-label' + = f.label :home_page_url, class: 'control-label col-sm-2' .col-sm-10 - = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com' - %span.help-block We will redirect non-logged in users to this page + = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' + %span.help-block#home_help_block We will redirect non-logged in users to this page .form-group - = f.label :sign_in_text, class: 'control-label' + = f.label :sign_in_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 931b0c5c10..d1c586328a 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -32,9 +32,9 @@ %span.light.pull-right = Milestone.count %p - Users who signed in during last 30 days + Active Users %span.light.pull-right - = User.where("current_sign_in_at > ?", 30.days.ago).count + = User.active.count .col-md-4 %h4 Features diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 34421edc06..19080089b8 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -14,7 +14,7 @@ .form-group .col-sm-2 .col-sm-10 - .bs-callout.bs-callout-info + .alert.alert-info = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create" diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b984188eb9..3a1e61d5d8 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -44,7 +44,7 @@ Projects (#{@projects.total_count}) .panel-head-actions .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? = sort_options_hash[@sort] @@ -63,7 +63,7 @@ = sort_title_oldest_updated = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do = sort_title_largest_repo - = link_to 'New Project', new_project_path, class: "btn btn-new" + = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success" %ul.well-list - @projects.each do |project| %li diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 5df8849317..291e48efc1 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -10,20 +10,59 @@ - @service.errors.full_messages.each do |msg| %p= msg - if @service.help.present? - .bs-callout + .alert.alert-info = preserve do = markdown @service.help + .form-group + = f.label :url, "Trigger", class: 'control-label' + - if @service.supported_events.length > 1 + .col-sm-10 + - if @service.supported_events.include?("push") + %div + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + - if @service.supported_events.include?("tag_push") + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("issue") + %div + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + - if @service.supported_events.include?("merge_request") + %div + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created + - @service.fields.each do |field| - name = field[:name] + - title = field[:title] || name.humanize - value = @service.send(name) unless field[:type] == 'password' - type = field[:type] - placeholder = field[:placeholder] - choices = field[:choices] - default_choice = field[:default_choice] + - help = field[:help] .form-group - = f.label name, class: "control-label" + = f.label name, title, class: "control-label" .col-sm-10 - if type == 'text' = f.text_field name, class: "form-control", placeholder: placeholder @@ -35,6 +74,8 @@ = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' = f.password_field name, class: 'form-control' + - if help + %span.help-block= help .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index cd96bb4477..680bd9ceb9 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -36,7 +36,7 @@ .panel-head-actions = link_to 'Send email to users', admin_email_path, class: 'btn btn-info' .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} %span.light sort: - if @sort.present? = sort_options_hash[@sort] @@ -60,7 +60,7 @@ = link_to admin_users_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" %ul.well-list - @users.each do |user| %li diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 9026789750..90c9f8c2f9 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -108,45 +108,49 @@ .col-md-6 - unless @user == current_user - if @user.blocked? - .alert.alert-info - %h4 This user is blocked - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-new", data: { confirm: 'Are you sure?' } + .panel.panel-info + .panel-heading + This user is blocked + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - else - .alert.alert-warning - %h4 Block this user - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-remove" + .panel.panel-warning + .panel-heading + Block this user + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li User will be removed from joined projects and groups + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" - .alert.alert-danger - %h4 + .panel.panel-danger + .panel-heading Remove user - %p Deleting a user has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = @user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if @user.solo_owned_groups.present? - %li - Next groups with all content will be removed: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} - %br - = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + .panel-body + %p Deleting a user has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = @user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + - if @user.solo_owned_groups.present? + %li + Next groups with all content will be removed: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %br + = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" #profile.tab-pane .row diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml deleted file mode 100644 index e3df43d889..0000000000 --- a/app/views/dashboard/_groups.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -.panel.panel-default - .panel-heading.clearfix - .input-group - = search_field_tag :filter_group, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' - - if current_user.can_create_group? - .input-group-addon.dash-new-group - = link_to new_group_path, class: "" do - %strong New group - %ul.well-list.dash-list - - groups.each do |group| - %li.group-row - = link_to group_path(id: group.path), class: dom_class(group) do - .dash-project-avatar - = image_tag group_icon(group.path), class: "avatar s40" - %span.group-name.filter-title - = truncate(group.name, length: 35) - %span.arrow - %i.fa.fa-angle-right - - if groups.blank? - %li - .nothing-here-block You have no groups yet. diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml deleted file mode 100644 index fa9179cb24..0000000000 --- a/app/views/dashboard/_project.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= link_to project_path(project), class: dom_class(project) do - .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) - %span.str-truncated - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - %span.arrow - %i.fa.fa-angle-right diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 0596738342..3634b2bfd7 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -1,25 +1,10 @@ .panel.panel-default .panel-heading.clearfix .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? .input-group-addon.dash-new-project = link_to new_project_path do %strong New project - %ul.well-list.dash-list - - projects.each do |project| - %li.project-row - = render "project", project: project - - - if projects.blank? - %li - .nothing-here-block There are no projects here. - - if @projects_count > @projects_limit - %li.bottom - %span.light - #{@projects_limit} of #{pluralize(@projects_count, 'project')} displayed. - .pull-right - = link_to projects_dashboard_path do - Show all - %i.fa.fa-angle-right + = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/dashboard/_projects_filter.html.haml b/app/views/dashboard/_projects_filter.html.haml deleted file mode 100644 index d87ca861ae..0000000000 --- a/app/views/dashboard/_projects_filter.html.haml +++ /dev/null @@ -1,100 +0,0 @@ -.dash-projects-filters.append-bottom-20 - .append-right-20 - %ul.nav.nav-tabs - = nav_tab :scope, nil do - = link_to projects_dashboard_filter_path(scope: nil) do - All - = nav_tab :scope, 'personal' do - = link_to projects_dashboard_filter_path(scope: 'personal') do - Personal - = nav_tab :scope, 'joined' do - = link_to projects_dashboard_filter_path(scope: 'joined') do - Joined - = nav_tab :scope, 'owned' do - = link_to projects_dashboard_filter_path(scope: 'owned') do - Owned - - .dropdown.inline.append-right-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-globe - %span.light Visibility: - - if params[:visibility_level].present? - = visibility_level_label(params[:visibility_level].to_i) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(visibility_level: nil) do - Any - - Gitlab::VisibilityLevel.values.each do |level| - %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(visibility_level: level) do - = visibility_level_icon(level) - = visibility_level_label(level) - - - if @groups.present? - .dropdown.inline.append-right-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-group - %span.light Group: - - if params[:group].present? - = Group.find_by(name: params[:group]).name - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(group: nil) do - Any - - @groups.each do |group| - %li{ class: (group.name == params[:group]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(group: group.name) do - = group.name - %small.pull-right - = group.projects.count - - - - - if @tags.present? - .dropdown.inline.append-right-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-tags - %span.light Tags: - - if params[:tag].present? - = params[:tag] - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(tag: nil) do - Any - - - @tags.each do |tag| - %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(tag: tag.name) do - %i.fa.fa-tag - = tag.name - - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to projects_dashboard_filter_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to projects_dashboard_filter_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to projects_dashboard_filter_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to projects_dashboard_filter_path(sort: sort_value_name) do - = sort_title_name diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml index a980f49542..78f695be91 100644 --- a/app/views/dashboard/_sidebar.html.haml +++ b/app/views/dashboard/_sidebar.html.haml @@ -1,18 +1,3 @@ -%ul.nav.nav-tabs.dash-sidebar-tabs - %li.active - = link_to '#projects', 'data-toggle' => 'tab', id: 'sidebar-projects-tab' do - Projects - %span.badge= @projects_count - %li - = link_to '#groups', 'data-toggle' => 'tab', id: 'sidebar-groups-tab' do - Groups - %span.badge= @groups.count - -.tab-content - .tab-pane.active#projects - = render "projects", projects: @projects - .tab-pane#groups - = render "groups", groups: @groups - += render "dashboard/projects", projects: @projects .prepend-top-20 = render 'shared/promo' diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index 6e76f95b34..4e7d663972 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -1,3 +1,4 @@ +- publicish_project_count = Project.publicish(current_user).count %h3.page-title Welcome to GitLab! %p.light Self hosted Git management application. %hr @@ -35,7 +36,7 @@ %i.fa.fa-plus New Group --if @publicish_project_count > 0 +-if publicish_project_count > 0 %hr %div .dashboard-intro-icon @@ -43,7 +44,7 @@ .dashboard-intro-text %p.slead There are - %strong= @publicish_project_count + %strong= publicish_project_count public projects on this server. %br Public projects are an easy way to allow everyone to have read-only access. diff --git a/app/views/profiles/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml similarity index 79% rename from app/views/profiles/groups/index.html.haml rename to app/views/dashboard/groups/index.html.haml index daf76636ff..50e90b1c17 100644 --- a/app/views/profiles/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -23,10 +23,11 @@ Settings - if can?(current_user, :destroy, user_group) - = link_to leave_profile_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do + = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do %i.fa.fa-sign-out Leave + = image_tag group_icon(group.path), class: "avatar s40 avatar-tile" = link_to group, class: 'group-name' do %strong= group.name diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml new file mode 100644 index 0000000000..f689b9698e --- /dev/null +++ b/app/views/dashboard/milestones/_issue.html.haml @@ -0,0 +1,10 @@ +%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } + %span.milestone-row + - project = issue.project + %strong #{project.name_with_namespace} · + = link_to [project.namespace.becomes(Namespace), project, issue] do + %span.cgray ##{issue.iid} + = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title + .pull-right.assignee-icon + - if issue.assignee + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml new file mode 100644 index 0000000000..9f350b772b --- /dev/null +++ b/app/views/dashboard/milestones/_issues.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading= title + %ul{ class: "well-list issues-sortable-list" } + - if issues + - issues.each do |issue| + = render 'issue', issue: issue diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml new file mode 100644 index 0000000000..8f5c4cce52 --- /dev/null +++ b/app/views/dashboard/milestones/_merge_request.html.haml @@ -0,0 +1,10 @@ +%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } + %span.milestone-row + - project = merge_request.project + %strong #{project.name_with_namespace} · + = link_to [project.namespace.becomes(Namespace), project, merge_request] do + %span.cgray ##{merge_request.iid} + = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title + .pull-right.assignee-icon + - if merge_request.assignee + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml new file mode 100644 index 0000000000..50057e2c63 --- /dev/null +++ b/app/views/dashboard/milestones/_merge_requests.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading= title + %ul{ class: "well-list merge_requests-sortable-list" } + - if merge_requests + - merge_requests.each do |merge_request| + = render 'merge_request', merge_request: merge_request diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml new file mode 100644 index 0000000000..caf3b68586 --- /dev/null +++ b/app/views/dashboard/milestones/index.html.haml @@ -0,0 +1,38 @@ +%h3.page-title + Milestones + %span.pull-right #{@dashboard_milestones.count} milestones + +%p.light + List all milestones from all projects you have access to. + +%hr + += render 'shared/milestones_filter' +.milestones + .panel.panel-default + %ul.well-list + - if @dashboard_milestones.blank? + %li + .nothing-here-block No milestones to show + - else + - @dashboard_milestones.each do |milestone| + %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + %h4 + = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) + %div + %div + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' +   + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' +   + %span.light #{milestone.percent_complete}% complete + = milestone_progress_bar(milestone) + %div + %br + - milestone.milestones.each do |milestone| + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do + %span.label.label-default + = milestone.project.name_with_namespace + = paginate @dashboard_milestones, theme: "gitlab" diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml new file mode 100644 index 0000000000..57cce9ab74 --- /dev/null +++ b/app/views/dashboard/milestones/show.html.haml @@ -0,0 +1,81 @@ +%h4.page-title + .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } + - if @dashboard_milestone.closed? + Closed + - else + Open + Milestone #{@dashboard_milestone.title} + +%hr +- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active? + .alert.alert-success + %span All issues for this milestone are closed. You may close the milestone now. + +.description +%table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - @dashboard_milestone.milestones.each do |milestone| + %tr + %td + = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) + %td + = milestone.issues.opened.count + %td + - if milestone.closed? + Closed + - else + Open + %td + = milestone.expires_at + +.context + %p.lead + Progress: + #{@dashboard_milestone.closed_items_count} closed + – + #{@dashboard_milestone.open_items_count} open + = milestone_progress_bar(@dashboard_milestone) + +%ul.nav.nav-tabs + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab' do + Issues + %span.badge= @dashboard_milestone.issue_count + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do + Merge Requests + %span.badge= @dashboard_milestone.merge_requests_count + %li + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= @dashboard_milestone.participants.count + +.tab-content + .tab-pane.active#tab-issues + .row + .col-md-6 + = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues + .col-md-6 + = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues + + .tab-pane#tab-merge-requests + .row + .col-md-6 + = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests + .col-md-6 + = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests + + .tab-pane#tab-participants + %ul.bordered-list + - @dashboard_milestone.participants.each do |user| + %li + = link_to user, title: user.name, class: "darken" do + = image_tag avatar_icon(user.email, 32), class: "avatar s32" + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.username diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml deleted file mode 100644 index 03d4b3d8bb..0000000000 --- a/app/views/dashboard/projects.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -%h3.page-title - My Projects - - = link_to new_project_path, class: "btn btn-new pull-right" do - %i.fa.fa-plus - New Project - -%p.light - All projects you have access to are listed here. Public projects are not included here unless you are a member -%hr -.side-filters - = render "projects_filter" -.dash-projects - %ul.bordered-list.my-projects.top-list - - @projects.each do |project| - %li.my-project-row - %h4.project-title - .pull-left - = project_icon(project, alt: '', class: 'avatar project-avatar s60') - .project-access-icon - = visibility_level_icon(project.visibility_level) - = link_to project_path(project), class: dom_class(project) do - %strong= project.name_with_namespace - - - if project.forked_from_project -   - %small - %i.fa.fa-code-fork - Forked from: - = link_to project.forked_from_project.name_with_namespace, namespace_project_path(project.namespace, project.forked_from_project) - - - if current_user.can_leave_project?(project) - .pull-right - = link_to leave_namespace_project_team_members_path(project.namespace, project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do - %i.fa.fa-sign-out - Leave - - .project-info - .pull-right - - if project.archived? - %span.label - %i.fa.fa-archive - Archived - - project.tags.each do |tag| - %span.label.label-info - %i.fa.fa-tag - = tag.name - - if project.description.present? - %p= truncate project.description, length: 100 - .last-activity - %span.light Last activity: - %span.date= project_last_activity(project) - - - - if @projects.blank? - %li - .nothing-here-block There are no projects here. - .bottom - = paginate @projects, theme: "gitlab" - diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml new file mode 100644 index 0000000000..94de609256 --- /dev/null +++ b/app/views/dashboard/projects/starred.html.haml @@ -0,0 +1,23 @@ +- if @projects.any? + .dashboard.row + %section.activities.col-md-8 + = render 'dashboard/activities' + %aside.col-md-4 + .panel.panel-default + .panel-heading.clearfix + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if current_user.can_create_project? + .input-group-addon.dash-new-project + = link_to new_project_path do + %strong New project + + = render 'shared/projects_list', projects: @projects, + projects_limit: 20, stars: true, avatar: false + + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left + +- else + %h3 You dont have starred projects yet + %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index f973f4829a..fa8946011b 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -1,4 +1,4 @@ -- if @has_authorized_projects +- if @projects.any? .dashboard.row %section.activities.col-md-8 = render 'activities' diff --git a/app/views/events/_events.html.haml b/app/views/events/_events.html.haml index 3d62d47886..68c19df092 100644 --- a/app/views/events/_events.html.haml +++ b/app/views/events/_events.html.haml @@ -1 +1 @@ -= render @events += render partial: 'events/event', collection: @events diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml new file mode 100644 index 0000000000..b3963a9d90 --- /dev/null +++ b/app/views/explore/projects/_filter.html.haml @@ -0,0 +1,67 @@ +.pull-left + = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| + .form-group + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" + .form-group + = button_tag 'Search', class: "btn btn-primary wide" + +.pull-right.hidden-sm.hidden-xs + - if current_user + .dropdown.inline.append-right-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-globe + %span.light Visibility: + - if params[:visibility_level].present? + = visibility_level_label(params[:visibility_level].to_i) + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(visibility_level: nil) do + Any + - Gitlab::VisibilityLevel.values.each do |level| + %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } + = link_to explore_projects_filter_path(visibility_level: level) do + = visibility_level_icon(level) + = visibility_level_label(level) + + - if @tags.present? + .dropdown.inline.append-right-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-tags + %span.light Tags: + - if params[:tag].present? + = params[:tag] + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(tag: nil) do + Any + + - @tags.each do |tag| + %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } + = link_to explore_projects_filter_path(tag: tag.name) do + %i.fa.fa-tag + = tag.name + + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index cb93b300d6..5086b58cd0 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,30 +1,5 @@ .clearfix - .pull-left - = form_tag explore_projects_path, method: :get, class: 'form-inline form-tiny' do |f| - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" - .form-group - = button_tag 'Search', class: "btn btn-primary wide" - - .pull-right - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to explore_projects_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to explore_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to explore_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to explore_projects_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated + = render 'filter' %hr .public-projects diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index b505760fa8..6f53e125c4 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,23 +1,10 @@ .panel.panel-default - .panel-heading - Projects (#{projects.count}) - - if can? current_user, :create_projects, @group - .panel-head-actions - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do - %i.fa.fa-plus - New project - %ul.well-list - - if projects.blank? - .nothing-here-block This group has no projects yet - - projects.each do |project| - %li.project-row - = link_to project_path(project), class: dom_class(project) do - .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar s40') - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) - %span.str-truncated - %span.project-name - = project.name - %span.arrow - %i.fa.fa-angle-right + .panel-heading.clearfix + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if can? current_user, :create_projects, @group + .input-group-addon.dash-new-project + = link_to new_project_path(namespace_id: @group.id) do + %strong New project + + = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 21029c3a07..6267006f63 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -8,6 +8,9 @@ %span.cgray= user.username - if user == current_user %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked - if show_roles %span.pull-right @@ -19,7 +22,7 @@ %i.fa.fa-pencil-square-o - if can?(current_user, :destroy, member) - if current_user == member.user - = link_to leave_profile_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 7f0b2832ca..9febaab04a 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -36,11 +36,11 @@ = pluralize milestone.merge_requests_count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} + = milestone_progress_bar(milestone) %div %br - - milestone.projects.each do |project| - %span.label.label-default - = project.name + - milestone.milestones.each do |milestone| + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do + %span.label.label-default + = milestone.project.name = paginate @group_milestones, theme: "gitlab" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index e3606d167a..dd2d84499b 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -45,8 +45,7 @@ #{@group_milestone.closed_items_count} closed – #{@group_milestone.open_items_count} open - .progress.progress-info - .progress-bar{style: "width: #{@group_milestone.percent_complete}%;"} + = milestone_progress_bar(@group_milestone) %ul.nav.nav-tabs %li.active diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index d5e6cb46f3..976868ec8f 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -4,7 +4,7 @@ projects: - if can? current_user, :manage_group, @group .panel-head-actions - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do + = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do %i.fa.fa-plus New Project %ul.well-list diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml new file mode 100644 index 0000000000..58de5b7c86 --- /dev/null +++ b/app/views/help/ui.html.haml @@ -0,0 +1,208 @@ +- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." + +.gitlab-ui-dev-kit + %h1 GitLab UI development kit + %p.light + Use page inspector in your browser to check element classes and structure + of examples below. + %hr + %ul + %li + = link_to 'Blocks', '#blocks' + %li + = link_to 'Lists', '#lists' + %li + = link_to 'Tables', '#tables' + %li + = link_to 'Buttons', '#buttons' + %li + = link_to 'Panels', '#panels' + %li + = link_to 'Alerts', '#alerts' + %li + = link_to 'Forms', '#forms' + %li + = link_to 'Markdown', '#markdown' + + %h2#blocks Blocks + + %h3 + %code .well + + + .well + %h4 Something + = lorem + + + %h2#lists Lists + + %h3 + %code .well-list + %ul.well-list + %li + One item + %li + One item + %li + One item + + %h3 + %code .panel .well-list + + .panel.panel-default + .panel-heading My list + %ul.well-list + %li + One item + %li + One item + %li + One item + + %h3 + %code .bordered-list + %ul.bordered-list + %li + One item + %li + One item + %li + One item + + + + %h2#tables Tables + + .example + %table.table + %thead + %tr + %th # + %th First Name + %th Last Name + %th Username + %tbody + %tr + %td 1 + %td Mark + %td Otto + %td @mdo + %tr + %td 2 + %td Jacob + %td Thornton + %td @fat + %tr + %td 3 + %td Larry + %td the Bird + %td @twitter + + + %h2#buttons Buttons + + .example + %button.btn.btn-default{:type => "button"} Default + %button.btn.btn-primary{:type => "button"} Primary + %button.btn.btn-success{:type => "button"} Success + %button.btn.btn-info{:type => "button"} Info + %button.btn.btn-warning{:type => "button"} Warning + %button.btn.btn-danger{:type => "button"} Danger + %button.btn.btn-link{:type => "button"} Link + + %h2#panels Panels + + .row + .col-md-6 + .panel.panel-success + .panel-heading Success + .panel-body + = lorem + .panel.panel-primary + .panel-heading Primary + .panel-body + = lorem + .panel.panel-info + .panel-heading Info + .panel-body + = lorem + .col-md-6 + .panel.panel-warning + .panel-heading Warning + .panel-body + = lorem + .panel.panel-danger + .panel-heading Danger + .panel-body + = lorem + + %h2#alert Alerts + + .row + .col-md-6 + .alert.alert-success + = lorem + .alert.alert-primary + = lorem + .alert.alert-info + = lorem + .col-md-6 + .alert.alert-warning + = lorem + .alert.alert-danger + = lorem + + %h2#forms Forms + + %h3 + %code form.horizontal-form + + %form.form-horizontal + .form-group + %label.col-sm-2.control-label{:for => "inputEmail3"} Email + .col-sm-10 + %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/ + .form-group + %label.col-sm-2.control-label{:for => "inputPassword3"} Password + .col-sm-10 + %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/ + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + %label + %input{:type => "checkbox"}/ + Remember me + .form-group + .col-sm-offset-2.col-sm-10 + %button.btn.btn-default{:type => "submit"} Sign in + + %h3 + %code form + + %form + .form-group + %label{:for => "exampleInputEmail1"} Email address + %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/ + .form-group + %label{:for => "exampleInputPassword1"} Password + %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/ + .checkbox + %label + %input{:type => "checkbox"}/ + Remember me + %button.btn.btn-default{:type => "submit"} Sign in + + %h2#markdown Markdown + %h3 + %code .md or .wiki and others + + Markdown rendering has a bit different css and presented in next UI elements: + + %ul + %li comment + %li issue, merge request description + %li wiki page + %li help page + + You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown", "markdown")}. diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml index bd6bb3c720..3d6d2bfc00 100644 --- a/app/views/layouts/_public_head_panel.html.haml +++ b/app/views/layouts/_public_head_panel.html.haml @@ -12,7 +12,7 @@ - unless current_controller?('sessions') .pull-right.hidden-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new' + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10' .navbar-collapse.collapse %ul.nav.navbar-nav diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 48c7c99942..e4f630c6a1 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -3,12 +3,22 @@ = link_to root_path, title: 'Home', class: 'shortcuts-activity' do %i.fa.fa-dashboard %span - Activity - = nav_link(path: 'dashboard#projects') do - = link_to projects_dashboard_path, title: 'Projects', class: 'shortcuts-projects' do - %i.fa.fa-cube + Your Projects + = nav_link(path: 'projects#starred') do + = link_to starred_dashboard_projects_path, title: 'Starred Projects' do + %i.fa.fa-star %span - Projects + Starred Projects + = nav_link(controller: :groups) do + = link_to dashboard_groups_path, title: 'Groups' do + %i.fa.fa-group + %span + Groups + = nav_link(controller: :milestones) do + = link_to dashboard_milestones_path, title: 'Milestones' do + %i.fa.fa-clock-o + %span + Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle @@ -26,4 +36,3 @@ %i.fa.fa-question-circle %span Help - diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 0914d2a167..d88e862829 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -43,11 +43,6 @@ %i.fa.fa-image %span Design - = nav_link(controller: :groups) do - = link_to profile_groups_path, title: 'Groups' do - %i.fa.fa-group - %span - Groups = nav_link(path: 'profiles#history') do = link_to history_profile_path, title: 'History' do %i.fa.fa-history diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 8cca80e524..7eec93abdf 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -16,6 +16,18 @@ font-size:small; color:#777 } + pre.commit-message { + white-space: pre-wrap; + } + .file-stats a { + text-decoration: none; + } + .file-stats .new-file { + color: #090; + } + .file-stats .deleted-file { + color: #B00; + } #{add_email_highlight_css} %body %div.content @@ -27,5 +39,5 @@ - if @target_url #{link_to "View it on GitLab", @target_url} = email_action @target_url - - if @project + - if @project && !@disable_footer You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team. diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index a45d1dedcd..039b92df2b 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,6 +1,12 @@ %h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} -%h4 Commits: +- if @reverse_compare + %p + %strong WARNING: + The push did not contain any new commits, but force pushed to delete the commits and changes below. + +%h4 + = @reverse_compare ? "Deleted commits:" : "Commits:" %ul - @commits.each do |commit| @@ -9,22 +15,52 @@ %div %span by #{commit.author_name} %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} - %pre #{commit.safe_message} + %pre.commit-message + = commit.safe_message -%h4 Changes: -- @diffs.each do |diff| - %li - %strong - - if diff.old_path == diff.new_path - = diff.new_path - - elsif diff.new_path && diff.old_path - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path || diff.old_path - %hr - %pre - = color_email_diff(diff.diff) - %br +%h4 #{pluralize @diffs.count, "changed file"}: + +%ul + - @diffs.each_with_index do |diff, i| + %li.file-stats + %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } + - if diff.deleted_file + %span.deleted-file + − + = diff.old_path + - elsif diff.renamed_file + = diff.old_path + → + = diff.new_path + - elsif diff.new_file + %span.new-file + + + = diff.new_path + - else + = diff.new_path + +- unless @disable_diffs + %h4 Changes: + - @diffs.each_with_index do |diff, i| + %li{id: "diff-#{i}"} + %a{href: @target_url + "#diff-#{i}"} + - if diff.deleted_file + %strong + = diff.old_path + deleted + - elsif diff.renamed_file + %strong + = diff.old_path + → + %strong + = diff.new_path + - else + %strong + = diff.new_path + %hr + %pre + = color_email_diff(diff.diff) + %br - if @compare.timeout %h5 Huge diff. To prevent performance issues changes are hidden diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index fa355cb526..8d67a42234 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,25 +1,47 @@ -#{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} - +#{@author.name} pushed to #{@branch} at #{@project.name_with_namespace} \ -Commits: +\ +- if @reverse_compare + WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. + \ + \ += @reverse_compare ? "Deleted commits:" : "Commits:" - @commits.each do |commit| - #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} by #{commit.author_name} + #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} #{commit.safe_message} \- - - - - \ \ -Changes: -- @diffs.each do |diff| - \ - \===================================== - - if diff.old_path == diff.new_path - = diff.new_path - - elsif diff.new_path && diff.old_path - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path || diff.old_path - \===================================== - != diff.diff +#{pluralize @diffs.count, "changed file"}: \ +- @diffs.each do |diff| + - if diff.deleted_file + \- − #{diff.old_path} + - elsif diff.renamed_file + \- #{diff.old_path} → #{diff.new_path} + - elsif diff.new_file + \- + #{diff.new_path} + - else + \- #{diff.new_path} +- unless @disable_diffs + \ + \ + Changes: + - @diffs.each do |diff| + \ + \===================================== + - if diff.deleted_file + #{diff.old_path} deleted + - elsif diff.renamed_file + #{diff.old_path} → #{diff.new_path} + - else + = diff.new_path + \===================================== + != diff.diff - if @compare.timeout + \ + \ Huge diff. To prevent performance issues it was hidden +\ +\ +View it on GitLab: #{@target_url} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index f124637c07..6bafcb5655 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -57,7 +57,7 @@ %p.light = user_url(@user) %div - = f.submit 'Save username', class: "btn btn-save" + = f.submit 'Save username', class: "btn btn-warning" - if show_profile_remove_tab? %fieldset.remove-account diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index e3cd323927..6cf5c81c19 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -50,7 +50,7 @@ %p You will receive all notifications from projects in which you participate .form-actions - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-create" .clearfix %hr diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 3b1ebbfaf5..4b04b113e8 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -35,4 +35,4 @@ .col-sm-10 = f.password_field :password_confirmation, required: true, class: 'form-control' .form-actions - = f.submit 'Save password', class: "btn btn-save" + = f.submit 'Save password', class: "btn btn-create" diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index b2808c46c0..1a7bc353bf 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -56,7 +56,7 @@ .form-group = f.label :bio, class: "control-label" .col-sm-10 - = f.text_area :bio, rows: 6, class: "form-control", maxlength: 250 + = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250 %span.help-block Tell us about yourself in fewer than 250 characters. .col-md-5 @@ -89,10 +89,12 @@ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" - if @user.public_profile? - .bs-callout.bs-callout-info + .alert.alert-info %h4 Public profile %p Your profile is publicly visible because you joined public project(s) - .form-actions - = f.submit 'Save changes', class: "btn btn-save" + .row + .col-md-7 + .col-sm-2 + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index 5c52f91927..07d4d60276 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -10,4 +10,4 @@ you need to - else your GitLab administrator needs to - == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/butbucket.md'}. \ No newline at end of file + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'}. diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index bfacab5e48..a7cd129b63 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -50,6 +50,7 @@ = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, { class: 'select2' }) - else + .prepend-top-10 %span.light No open milestones available.   - if can? current_user, :admin_milestone, issuable.project @@ -63,6 +64,7 @@ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2' - else + .prepend-top-10 %span.light No labels yet.   - if can? current_user, :admin_label, issuable.project diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 64cc3fad6c..9ff61f3887 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -15,7 +15,7 @@ - else = link_to title, '#' -%ul.blob-commit-info.bs-callout.bs-callout-info.hidden-xs +%ul.blob-commit-info.well.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 20e51d18da..9b5eb84a86 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -26,7 +26,7 @@ %a{href: "#diff-#{i}"} %i.fa.fa-minus = diff.old_path - \-> + → = diff.new_path - elsif diff.new_file %span.new-file diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index c9a6b3ebd9..af1f342afb 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -1,4 +1,4 @@ -.bs-callout.bs-callout-warning +.alert.alert-warning %h4 Too many changes. .pull-right diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index a189a48713..4a5d09b950 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,7 +1,7 @@ = render 'head' %p.lead - Commits statistic for + Commit statistics for %strong #{@repository.root_ref} #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 097374e112..f1248ac2af 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -12,7 +12,7 @@ %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info + .alert.alert-info This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7defc8787a..cbbcb1d06c 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -25,11 +25,11 @@ .clearfix .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status") = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :status, params[:status] + = hidden_field_tag :state_event, params[:state_event] = button_tag "Update issues", class: "btn update_selected_issues btn-save" .issues-holder diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 95912536e4..2305fce112 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -2,7 +2,7 @@ -if @label.errors.any? .row .col-sm-10.col-sm-offset-2 - .bs-callout.bs-callout-danger + .alert.alert-danger - @label.errors.full_messages.each do |msg| %span= msg %br diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 73eccfa556..bf80afe878 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -99,11 +99,11 @@ - if @diffs.present? = render "projects/diffs/diffs", diffs: @diffs, project: @project - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .bs-callout.bs-callout-danger + .alert.alert-danger %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - .bs-callout.bs-callout-danger + .alert.alert-danger %h4 This comparison includes a huge diff. %p To preserve performance the line changes are not shown. diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index cfef1d5e4c..786b5f3906 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -3,7 +3,7 @@ - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else - .bs-callout.bs-callout-warning + .alert.alert-warning %h4 Changes view for this comparison is extremely large. %p diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index ee7fd0ef15..85a7103f3b 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -3,21 +3,21 @@ %i.fa.fa-check %span CI build passed for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget.ci-failed{style: "display:none"} %i.fa.fa-times %span CI build failed for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - [:running, :pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} %i.fa.fa-clock-o %span CI build #{status} for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget %i.fa.fa-spinner diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml index 46f2df1b18..42fbd0cd2c 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -3,3 +3,6 @@ = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do %span.cgray ##{merge_request.iid} = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title + .pull-right.assignee-icon + - if merge_request.assignee + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index d32b2ba271..dcf56541db 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -23,5 +23,4 @@ = pluralize milestone.merge_requests.count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} + = milestone_progress_bar(milestone) diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index fea96f3701..110d896734 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -43,8 +43,7 @@   %span.light #{@milestone.percent_complete}% complete %span.pull-right= @milestone.expires_at - .progress.progress-info - .progress-bar{style: "width: #{@milestone.percent_complete}%;"} + = milestone_progress_bar(@milestone) %ul.nav.nav-tabs diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 025c4fd550..00b912742b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -34,7 +34,7 @@ %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info + .alert.alert-info.prepend-top-10 This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. @@ -65,7 +65,7 @@ %i.fa.fa-bitbucket Import projects from Bitbucket = render 'bitbucket_import_modal' - + - unless request.host == 'gitlab.com' .project-import.form-group .col-sm-2 diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index dc20e96732..cfe2808417 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -2,7 +2,7 @@ %p.light Keep stable branches secure and force developers to use Merge Requests %hr -.bs-callout.bs-callout-info +.alert.alert-info %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 0ebf2f78fe..3492dd5bab 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -18,7 +18,7 @@ %li= msg - if @service.help.present? - .bs-callout + .alert.alert-info = preserve do = markdown @service.help @@ -27,16 +27,63 @@ .col-sm-10 = f.check_box :active + .form-group + = f.label :url, "Trigger", class: 'control-label' + - if @service.supported_events.length > 1 + .col-sm-10 + - if @service.supported_events.include?("push") + %div + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + - if @service.supported_events.include?("tag_push") + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("note") + %div + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + - if @service.supported_events.include?("issue") + %div + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + - if @service.supported_events.include?("merge_request") + %div + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created + - @service.fields.each do |field| - name = field[:name] + - title = field[:title] || name.humanize - value = service_field_value(field[:type], @service.send(name)) - type = field[:type] - placeholder = field[:placeholder] - choices = field[:choices] - default_choice = field[:default_choice] + - help = field[:help] .form-group - = f.label name, class: "control-label" + = f.label name, title, class: "control-label" .col-sm-10 - if type == 'text' = f.text_field name, class: "form-control", placeholder: placeholder @@ -48,6 +95,8 @@ = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' = f.password_field name, placeholder: value, class: 'form-control' + - if help + %span.help-block= help .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 787cfd9304..74b0739565 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -40,7 +40,7 @@ %p Repository is read-only - if @project.forked_from_project - .alert.alert-success + .well %i.fa.fa-code-fork.project-fork-icon Forked from: %br diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml index 9de61afd0f..eb81544740 100644 --- a/app/views/projects/team_members/_team_member.html.haml +++ b/app/views/projects/team_members/_team_member.html.haml @@ -12,4 +12,7 @@ = image_tag avatar_icon(user.email, 32), class: "avatar s32" %p %strong= user.name + - if user.blocked? + %label.label.label-danger + %strong Blocked %span.cgray= user.username diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 5875f71bac..b34dd53e3b 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -15,7 +15,7 @@ = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false - if @group.persisted? - .bs-callout.bs-callout-danger + .alert.alert-danger %ul %li Changing group path can have unintended side effects. %li Renaming group path will rename directory for all related projects diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index 1a2946bacc..089179e677 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? .no-ssh-key-message.alert.alert-warning.hidden-xs - You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile + You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path, class: 'alert-link'} to your profile .pull-right - = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put + = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | - = link_to 'Remind later', '#', class: 'hide-no-ssh-message' + = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml new file mode 100644 index 0000000000..8746970c23 --- /dev/null +++ b/app/views/shared/_project.html.haml @@ -0,0 +1,21 @@ += cache [project, controller.controller_name, controller.action_name] do + = link_to project_path(project), class: dom_class(project) do + - if avatar + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + .dash-project-access-icon + = visibility_level_icon(project.visibility_level) + %span.str-truncated + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + - if stars + %span.pull-right.light + %i.fa.fa-star + = project.star_count + - else + %span.arrow + %i.fa.fa-angle-right diff --git a/app/views/shared/_projects_list.html.haml b/app/views/shared/_projects_list.html.haml new file mode 100644 index 0000000000..4c58092af4 --- /dev/null +++ b/app/views/shared/_projects_list.html.haml @@ -0,0 +1,17 @@ +- projects_limit = 20 unless local_assigns[:projects_limit] +- avatar = true unless local_assigns[:avatar] == false +- stars = false unless local_assigns[:stars] == true +%ul.well-list.projects-list + - projects.each_with_index do |project, i| + %li{class: (i >= projects_limit) ? 'project-row hide' : 'project-row'} + = render "shared/project", project: project, avatar: avatar, stars: stars + - if projects.blank? + %li + .nothing-here-block There are no projects here. + - if projects.count > projects_limit + %li.bottom + %span.light + #{projects_limit} of #{pluralize(projects.count, 'project')} displayed. + %span + = link_to '#', class: 'js-expand' do + Show all diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index 3b44959baa..0a70b73807 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -12,7 +12,7 @@ - unless user.linkedin.blank? %li %span.light LinkedIn: - %strong= user.linkedin + %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}" - unless user.twitter.blank? %li %span.light Twitter: diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index c925a48f55..6c7779be30 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,21 +1,13 @@ - if @contributed_projects.present? .panel.panel-default .panel-heading Projects contributed to - %ul.well-list - - @contributed_projects.sort_by(&:star_count).reverse.each do |project| - %li - = link_to_project project - %span.pull-right.light - %i.fa.fa-star - = project.star_count + = render 'shared/projects_list', + projects: @contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false - if @projects.present? .panel.panel-default .panel-heading Personal projects - %ul.well-list - - @projects.sort_by(&:star_count).reverse.each do |project| - %li - = link_to_project project - %span.pull-right.light - %i.fa.fa-star - = project.star_count + = render 'shared/projects_list', + projects: @projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 5e82d5780c..abd6b22978 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,7 @@ .row - .col-md-8 + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left + %section.col-md-8 %h3.page-title = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' = @user.name @@ -19,10 +21,11 @@ = render 'groups', groups: @groups %hr - .user-calendar - %h4.center.light - %i.fa.fa-spinner.fa-spin - %hr + .hidden-xs + .user-calendar + %h4.center.light + %i.fa.fa-spinner.fa-spin + %hr %h4 User Activity @@ -33,7 +36,7 @@ %i.fa.fa-rss = render @events - .col-md-4 + %aside.col-md-4 = render 'profile', user: @user = render 'projects' diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index e3f6f3a6ae..e59ca81def 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,25 +1,41 @@ class EmailsOnPushWorker include Sidekiq::Worker - def perform(project_id, recipients, push_data) + def perform(project_id, recipients, push_data, send_from_committer_email = false, disable_diffs = false) project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] branch = push_data["ref"] author_id = push_data["user_id"] - if before_sha =~ /^000000/ || after_sha =~ /^000000/ + if Gitlab::Git.blank_ref?(before_sha) || Gitlab::Git.blank_ref?(after_sha) # skip if new branch was pushed or branch was removed return true end compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - # Do not send emails if git compare failed - return false unless compare && compare.commits.present? + return false if compare.same + + if compare.commits.empty? + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + + reverse_compare = true + + return false if compare.commits.empty? + end recipients.split(" ").each do |recipient| - Notify.repository_push_email(project_id, recipient, author_id, branch, compare).deliver + Notify.repository_push_email( + project_id, + recipient, + author_id, + branch, + compare, + reverse_compare, + send_from_committer_email, + disable_diffs + ).deliver end ensure compare = nil diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 613bae351d..e1a99d9cad 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -57,9 +57,9 @@ class IrkerWorker end def send_branch_updates(push_data, project, repo_name, committer, branch) - if push_data['before'] =~ /^000000/ + if push_data['before'] == Gitlab::Git::BLANK_SHA send_new_branch project, repo_name, committer, branch - elsif push_data['after'] =~ /^000000/ + elsif push_data['after'] == Gitlab::Git::BLANK_SHA send_del_branch repo_name, committer, branch end end @@ -83,7 +83,7 @@ class IrkerWorker return if push_data['total_commits_count'] == 0 # Next message is for number of commit pushed, if any - if push_data['before'] =~ /^000000/ + if push_data['before'] == Gitlab::Git::BLANK_SHA # Tweak on push_data["before"] in order to have a nice compare URL push_data['before'] = before_on_new_branch push_data, project end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 1406cba2db..ecc6c8e53a 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -33,7 +33,7 @@ class PostReceive return false end - if tag?(ref) + if Gitlab::Git.tag_ref?(ref) GitTagPushService.new.execute(project, @user, oldrev, newrev, ref) else GitPushService.new.execute(project, @user, oldrev, newrev, ref) @@ -44,10 +44,4 @@ class PostReceive def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end - - private - - def tag?(ref) - !!(/refs\/tags\/(.*)/.match(ref)) - end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 42a617b628..2806239d4c 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -43,6 +43,7 @@ production: &base # email_enabled: true # Email address used in the "From" field in mails sent by GitLab email_from: example@example.com + email_display_name: GitLab # Email server smtp settings are in config/initializers/smtp_settings.rb.sample diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 12a33a6e1b..a489baed67 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -137,6 +137,7 @@ Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" +Settings.gitlab['email_display_name'] ||= "GitLab" Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' Settings.gitlab['user_home'] ||= begin diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index e00923e7e0..f0fe2fdfa4 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -3,6 +3,9 @@ # 2. Edit settings inside this file # 3. Restart GitLab instance # +# For full list of options and their values see http://api.rubyonrails.org/classes/ActionMailer/Base.html +# + if Rails.env.production? Gitlab::Application.config.action_mailer.delivery_method = :smtp @@ -14,6 +17,6 @@ if Rails.env.production? domain: "gitlab.company.com", authentication: :login, enable_starttls_auto: true, - openssl_verify_mode: 'none' + openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options } end diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb new file mode 100644 index 0000000000..bc88595cf2 --- /dev/null +++ b/config/initializers/timeout.rb @@ -0,0 +1,8 @@ +# Slowpoke extends Rack::Timeout to gracefully kill Unicorn workers so they can clean up state. +Slowpoke.timeout = 60 + +# The `Rack::Timeout` middleware kills requests after 60 seconds (as set above). +# We're replacing it with our `Gitlab::Middleware::Timeout` that does the same, +# except ignoring Git-over-HTTP requests, letting those take as long as they need. + +Rails.application.config.middleware.swap(Rack::Timeout, Gitlab::Middleware::Timeout) diff --git a/config/routes.rb b/config/routes.rb index c505c8d335..c76fa537dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,9 +7,8 @@ Gitlab::Application.routes.draw do authorized_applications: 'oauth/authorized_applications', authorizations: 'oauth/authorizations' end - # + # Search - # get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete @@ -33,14 +32,12 @@ Gitlab::Application.routes.draw do receive_pack: Gitlab.config.gitlab_shell.receive_pack }), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post] - # # Help - # - get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page get 'help/:category/*file' => 'help#show' get 'help/shortcuts' + get 'help/ui' => 'help#ui' # # Global snippets @@ -74,7 +71,7 @@ Gitlab::Application.routes.draw do get :callback get :jobs end - + resource :gitorious, only: [:create, :new], controller: :gitorious do get :status get :callback @@ -211,11 +208,6 @@ Gitlab::Application.routes.draw do end resources :keys resources :emails, only: [:index, :create, :destroy] - resources :groups, only: [:index] do - member do - delete :leave - end - end resource :avatar, only: [:destroy] end end @@ -231,10 +223,25 @@ Gitlab::Application.routes.draw do # resource :dashboard, controller: 'dashboard', only: [:show] do member do - get :projects get :issues get :merge_requests end + + scope module: :dashboard do + resources :milestones, only: [:index, :show] + + resources :groups, only: [:index] do + member do + delete :leave + end + end + + resources :projects, only: [] do + collection do + get :starred + end + end + end end # @@ -260,7 +267,7 @@ Gitlab::Application.routes.draw do resources :ldap_group_links, only: [:index, :create, :destroy] resources :group_members, only: [:create, :update, :destroy] resource :avatar, only: [:destroy] - resources :milestones + resources :milestones, only: [:index, :show, :update] end get "/audit_events" => "audit_events#group_log" diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index d8b4f5c7c3..29253b71f4 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -35,22 +35,10 @@ working_directory "/home/git/gitlab" # available in 0.94.0+ listen "/home/git/gitlab/tmp/sockets/gitlab.socket", :backlog => 1024 listen "127.0.0.1:8080", :tcp_nopush => true -# nuke workers after 30 seconds instead of 60 seconds (the default) -# -# NOTICE: git push over http depends on this value. -# If you want be able to push huge amount of data to git repository over http -# you will have to increase this value too. -# -# Example of output if you try to push 1GB repo to GitLab over http. -# -> git push http://gitlab.... master -# -# error: RPC failed; result=18, HTTP code = 200 -# fatal: The remote end hung up unexpectedly -# fatal: The remote end hung up unexpectedly -# -# For more information see http://stackoverflow.com/a/21682112/752049 -# -timeout 60 +# Kill workers after 1 hour. +# A shorter timeout of 60 seconds is enforced by rack-timeout for web requests. +# Git-over-HTTP only has the below timeout since large pulls/pushes can take a long time. +timeout 60 * 60 # feel free to point this anywhere accessible on the filesystem pid "/home/git/gitlab/tmp/pids/unicorn.pid" diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index b697f58d4e..24952a1f66 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -1,30 +1,31 @@ Gitlab::Seeder.quiet do (2..20).each do |i| begin - User.seed(:id, [{ - id: i, + User.create!( username: Faker::Internet.user_name, name: Faker::Name.name, email: Faker::Internet.email, - confirmed_at: DateTime.now - }]) + confirmed_at: DateTime.now, + password: '12345678' + ) + print '.' - rescue ActiveRecord::RecordNotSaved + rescue ActiveRecord::RecordInvalid print 'F' end end (1..5).each do |i| begin - User.seed do |s| - s.username = "user#{i}" - s.name = "User #{i}" - s.email = "user#{i}@example.com" - s.confirmed_at = DateTime.now - s.password = '12345678' - end + User.create!( + username: "user#{i}", + name: "User #{i}", + email: "user#{i}@example.com", + confirmed_at: DateTime.now, + password: '12345678' + ) print '.' - rescue ActiveRecord::RecordNotSaved + rescue ActiveRecord::RecordInvalid print 'F' end end diff --git a/db/migrate/20150219004514_add_events_to_services.rb b/db/migrate/20150219004514_add_events_to_services.rb new file mode 100644 index 0000000000..cf73a0174f --- /dev/null +++ b/db/migrate/20150219004514_add_events_to_services.rb @@ -0,0 +1,8 @@ +class AddEventsToServices < ActiveRecord::Migration + def change + add_column :services, :push_events, :boolean, :default => true + add_column :services, :issues_events, :boolean, :default => true + add_column :services, :merge_requests_events, :boolean, :default => true + add_column :services, :tag_push_events, :boolean, :default => true + end +end diff --git a/db/migrate/20150225065047_add_note_events_to_services.rb b/db/migrate/20150225065047_add_note_events_to_services.rb new file mode 100644 index 0000000000..d54ba9e482 --- /dev/null +++ b/db/migrate/20150225065047_add_note_events_to_services.rb @@ -0,0 +1,5 @@ +class AddNoteEventsToServices < ActiveRecord::Migration + def change + add_column :services, :note_events, :boolean, default: true, null: false + end +end diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb new file mode 100644 index 0000000000..334e557455 --- /dev/null +++ b/db/migrate/20150306023106_fix_namespace_duplication.rb @@ -0,0 +1,21 @@ +class FixNamespaceDuplication < ActiveRecord::Migration + def up + #fixes path duplication + select_all('SELECT MAX(id) max, COUNT(id) cnt, path FROM namespaces GROUP BY path HAVING COUNT(id) > 1').each do |nms| + bad_nms_ids = select_all("SELECT id FROM namespaces WHERE path = '#{nms['path']}' AND id <> #{nms['max']}").map{|x| x["id"]} + execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})") + execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})") + end + + #fixes name duplication + select_all('SELECT MAX(id) max, COUNT(id) cnt, name FROM namespaces GROUP BY name HAVING COUNT(id) > 1').each do |nms| + bad_nms_ids = select_all("SELECT id FROM namespaces WHERE name = '#{nms['name']}' AND id <> #{nms['max']}").map{|x| x["id"]} + execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})") + execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})") + end + end + + def down + # not implemented + end +end diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb new file mode 100644 index 0000000000..6472138e3e --- /dev/null +++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb @@ -0,0 +1,9 @@ +class AddUniqueIndexToNamespace < ActiveRecord::Migration + def change + remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) + remove_index :namespaces, column: :path if index_exists?(:namespaces, :path) + + add_index :namespaces, :name, unique: true + add_index :namespaces, :path, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 2352ec2fe5..5c6d2c3eb2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150225214822) do +ActiveRecord::Schema.define(version: 20150306023112) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -291,9 +291,9 @@ ActiveRecord::Schema.define(version: 20150225214822) do end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree + add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: true do |t| @@ -399,7 +399,7 @@ ActiveRecord::Schema.define(version: 20150225214822) do t.boolean "merge_requests_rebase_enabled", default: false t.string "import_type" t.string "import_source" - t.boolean "merge_requests_rebase_default", default: false + t.boolean "merge_requests_rebase_default", default: true end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -424,9 +424,14 @@ ActiveRecord::Schema.define(version: 20150225214822) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" - t.boolean "template", default: false + t.boolean "template", default: false + t.boolean "push_events", default: true + t.boolean "issues_events", default: true + t.boolean "merge_requests_events", default: true + t.boolean "tag_push_events", default: true + t.boolean "note_events", default: true, null: false end add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree diff --git a/doc/development/README.md b/doc/development/README.md index c31e5d7ae9..d5d264be19 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -5,3 +5,4 @@ - [Rake tasks](rake_tasks.md) for development - [CI setup](ci_setup.md) for testing GitLab - [Sidekiq debugging](sidekiq_debugging.md) +- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md new file mode 100644 index 0000000000..2f01defc11 --- /dev/null +++ b/doc/development/ui_guide.md @@ -0,0 +1,12 @@ +# UI Guide for building GitLab + +## Best practices for creating new pages in GitLab + +TODO: write some best practices when develop GitLab features. + +## GitLab UI development kit + +We created a page inside GitLab where you can check commonly used html and css elements. + +When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples +you can use during GitLab development. diff --git a/doc/integration/README.md b/doc/integration/README.md index af981d0fbb..23bdc1ebf5 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services. - [Slack](slack.md) Integrate with the Slack chat service - [Kerberos](kerberos.md) Integrate with Kerberos - [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). diff --git a/doc/integration/github.md b/doc/integration/github.md index a5c2913151..50eb22ca58 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -53,7 +53,7 @@ GitHub will generate an application ID and secret key for you to use. "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", "url" => "https://github.com/", - "args" => { "scope" => "user:email" } } + "args" => { "scope" => "user:email" } } ] ``` diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 49ffaa62af..216f1f11a9 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -58,7 +58,7 @@ GitLab.com will generate an application ID and secret key for you to use. "name" => "gitlab", "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", - "args" => { "scope" => "api" } } + "args" => { "scope" => "api" } } ] ``` @@ -81,4 +81,4 @@ GitLab.com will generate an application ID and secret key for you to use. On the sign in page there should now be a GitLab.com icon below the regular sign in form. Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to your GitLab instance and will be signed in. \ No newline at end of file +If everything goes well the user will be returned to your GitLab instance and will be signed in. diff --git a/doc/integration/google.md b/doc/integration/google.md index d7b741ece6..e1c14c7c94 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -55,7 +55,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application "name" => "google_oauth2", "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", - "args" => { "access_type" => "offline", "approval_prompt" => '' } } + "args" => { "access_type" => "offline", "approval_prompt" => '' } } ] ``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index dd44c1eb86..a7e5faf2a6 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -200,3 +200,7 @@ Consider creating a post on Hacker News. ## Update GitLab.com with the stable version - Deploy the package (should not need downtime because of the small difference with RC1) + +## Release new AMIs + +[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) diff --git a/doc/release/patch.md b/doc/release/patch.md index 80afa19b6c..5397343e71 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -54,3 +54,4 @@ CE=false be rake release['x.x.x'] 1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) 1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) +1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) diff --git a/doc/release/security.md b/doc/release/security.md index b67e0f37a0..6ed2526449 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -18,11 +18,12 @@ Please report suspected security vulnerabilities in private to ([a-zA-Z\-]+-)?\d+) # Issue ID |#{PROJ_STR}?!(?\d+) # MR ID |\$(?\d+) # Snippet ID + |(#{PROJ_STR}@)?(?[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range |(#{PROJ_STR}@)?(?[\h]{6,40}) # Commit ID |(?gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit ) (?\W)? # Suffix }x.freeze - TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit].freeze + TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze def parse_references(text, project = @project) # parse reference links @@ -290,6 +292,30 @@ module Gitlab end end + def reference_commit_range(identifier, project = @project, prefix_text = nil) + from_id, to_id = identifier.split(/\.{2,3}/, 2) + + inclusive = identifier !~ /\.{3}/ + from_id << "^" if inclusive + + if project.valid_repo? && + from = project.repository.commit(from_id) && + to = project.repository.commit(to_id) + + options = html_options.merge( + title: "Commits #{from_id} through #{to_id}", + class: "gfm gfm-commit_range #{html_options[:class]}" + ) + prefix_text = "#{prefix_text}@" if prefix_text + + link_to( + "#{prefix_text}#{identifier}", + namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id), + options + ) + end + end + def reference_external_issue(identifier, project = @project, prefix_text = nil) url = url_for_issue(identifier, project) diff --git a/lib/gitlab/middleware/timeout.rb b/lib/gitlab/middleware/timeout.rb new file mode 100644 index 0000000000..015600392b --- /dev/null +++ b/lib/gitlab/middleware/timeout.rb @@ -0,0 +1,13 @@ +module Gitlab + module Middleware + class Timeout < Rack::Timeout + GRACK_REGEX = /[-\/\w\.]+\.git\//.freeze + + def call(env) + return @app.call(env) if env['PATH_INFO'] =~ GRACK_REGEX + + super + end + end + end +end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb new file mode 100644 index 0000000000..644dec45dc --- /dev/null +++ b/lib/gitlab/note_data_builder.rb @@ -0,0 +1,77 @@ +module Gitlab + class NoteDataBuilder + class << self + # Produce a hash of post-receive data + # + # For all notes: + # + # data = { + # object_kind: "note", + # user: { + # name: String, + # username: String, + # avatar_url: String + # } + # project_id: Integer, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # } + # object_attributes: { + # + # } + # : { + # } + # note-specific data is a hash with one of the following keys and contains + # the hook data for that type. + # - commit + # - issue + # - merge_request + # - snippet + # + def build(note, user) + project = note.project + data = build_base_data(project, user, note) + + if note.for_commit? + data[:commit] = build_data_for_commit(project, user, note) + elsif note.for_issue? + data[:issue] = note.noteable.hook_attrs + elsif note.for_merge_request? + data[:merge_request] = note.noteable.hook_attrs + elsif note.for_project_snippet? + data[:snippet] = note.noteable.hook_attrs + end + + data + end + + def build_base_data(project, user, note) + base_data = { + object_kind: "note", + user: user.hook_attrs, + project_id: project.id, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + }, + object_attributes: note.hook_attrs + } + + base_data[:object_attributes][:url] = + Gitlab::UrlBuilder.new(:note).build(note.id) + base_data + end + + def build_data_for_commit(project, user, note) + # commit_id is the SHA hash + commit = project.repository.commit(note.commit_id) + commit.hook_attrs(project) + end + end + end +end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 9aa5c8967a..0cc6b0ac69 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -9,6 +9,7 @@ module Gitlab # ref: String, # user_id: String, # user_name: String, + # user_email: String # project_id: String, # repository: { # name: String, @@ -29,12 +30,14 @@ module Gitlab # Hash to be passed as post_receive_data data = { + object_kind: "push", before: oldrev, after: newrev, ref: ref, checkout_sha: checkout_sha(project.repository, newrev, ref), user_id: user.id, user_name: user.name, + user_email: user.email, project_id: project.id, repository: { name: project.name, @@ -55,6 +58,7 @@ module Gitlab data[:commits] << commit.hook_attrs(project) end + data[:commits] = "" if data[:commits].count == 0 data end @@ -62,12 +66,13 @@ module Gitlab # existing project and commits to test web hooks def build_sample(project, user) commits = project.repository.commits(project.default_branch, nil, 3) - build(project, user, commits.last.id, commits.first.id, "refs/heads/#{project.default_branch}", commits) + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" + build(project, user, commits.last.id, commits.first.id, ref, commits) end def checkout_sha(repository, newrev, ref) - if newrev != Gitlab::Git::BLANK_SHA && ref.start_with?('refs/tags/') - tag_name = Gitlab::Git.extract_ref_name(ref) + if newrev != Gitlab::Git::BLANK_SHA && Gitlab::Git.tag_ref?(ref) + tag_name = Gitlab::Git.ref_name(ref) tag = repository.find_tag(tag_name) if tag diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 9ba5e36a8b..16879501a0 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,13 +1,13 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits + attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits, :commit_ranges include Markdown def initialize - @users, @labels, @issues, @merge_requests, @snippets, @commits = - [], [], [], [], [], [] + @users, @labels, @issues, @merge_requests, @snippets, @commits, @commit_ranges = + [], [], [], [], [], [], [] end def analyze(string, project) @@ -62,6 +62,16 @@ module Gitlab end.reject(&:nil?) end + def commit_ranges_for(project = nil) + commit_ranges.map do |entry| + repo = entry[:project].repository if entry[:project] + if repo && should_lookup?(project, entry[:project]) + from_id, to_id = entry[:id].split(/\.{2,3}/, 2) + [repo.commit(from_id), repo.commit(to_id)] + end + end.reject(&:nil?) + end + private def reference_link(type, identifier, project, _) diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index ab7c8ad89f..6830d15875 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -13,6 +13,9 @@ module Gitlab build_issue_url(id) when :merge_request build_merge_request_url(id) + when :note + build_note_url(id) + end end @@ -27,5 +30,31 @@ module Gitlab merge_request = MergeRequest.find(id) merge_request_url(merge_request, host: Gitlab.config.gitlab['url']) end + + def build_note_url(id) + note = Note.find(id) + if note.for_commit? + namespace_project_commit_url(namespace_id: note.project.namespace, + id: note.commit_id, + project_id: note.project, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_issue? + issue = Issue.find(note.noteable_id) + issue_url(issue, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_merge_request? + merge_request = MergeRequest.find(note.noteable_id) + merge_request_url(merge_request, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_project_snippet? + snippet = Snippet.find(note.noteable_id) + snippet_url(snippet, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + end + end end end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 714261f815..713d7c39a1 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -3,8 +3,9 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML attr_reader :template alias_method :h, :template - def initialize(template, options = {}) + def initialize(template, color_scheme, options = {}) @template = template + @color_scheme = color_scheme @project = @template.instance_variable_get("@project") @options = options.dup super options @@ -34,7 +35,7 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end formatter = Rugments::Formatters::HTML.new( - cssclass: "code highlight white #{lexer.tag}" + cssclass: "code highlight #{@color_scheme} #{lexer.tag}" ) formatter.format(lexer.lex(code)) end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 43115915de..976c4b5f22 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -29,6 +29,7 @@ namespace :gitlab do check_redis_version check_ruby_version check_git_version + check_active_users finished_checking "GitLab" end @@ -781,6 +782,10 @@ namespace :gitlab do end end + def check_active_users + puts "Active users: #{User.active.count}" + end + def omnibus_gitlab? Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails' end diff --git a/public/503.html b/public/503.html new file mode 100644 index 0000000000..efdae0f512 --- /dev/null +++ b/public/503.html @@ -0,0 +1,13 @@ + + + + Page took too long to load (503) + + + +

503

+

Page took too long to load.

+
+

Please contact your GitLab administrator if this problem persists.

+ + diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb new file mode 100644 index 0000000000..0f9780356b --- /dev/null +++ b/spec/controllers/uploads_controller_spec.rb @@ -0,0 +1,296 @@ +require 'spec_helper' + +describe UploadsController do + let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + describe "GET show" do + context "when viewing a user avatar" do + context "when signed in" do + before do + sign_in(user) + end + + context "when the user is blocked" do + before do + user.block + end + + it "redirects to the sign in page" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when viewing a project avatar" do + let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when viewing a group avatar" do + let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let!(:project) { create(:project, namespace: group) } + + context "when the group has public projects" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project doesn't have public projects" do + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when viewing a note attachment" do + let!(:note) { create(:note, :with_attachment) } + let(:project) { note.project } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + end +end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 6ce1d7446f..77cd37c22d 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -40,7 +40,7 @@ FactoryGirl.define do source_branch "master" target_branch "feature" - merge_status :can_be_merged + merge_status "can_be_merged" trait :with_diffs do end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 83d0cc62db..f1c33461b5 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -30,6 +30,7 @@ FactoryGirl.define do factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_project_snippet, traits: [:on_project_snippet] trait :on_commit do project factory: :project @@ -52,6 +53,11 @@ FactoryGirl.define do noteable_type "Issue" end + trait :on_project_snippet do + noteable_id 1 + noteable_type "Snippet" + end + trait :with_attachment do attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index d1f00a3dd8..67238e3ab7 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -25,8 +25,8 @@ describe "Dashboard access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /dashboard/projects" do - subject { projects_dashboard_path } + describe "GET /dashboard/projects/starred" do + subject { starred_dashboard_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } @@ -52,4 +52,12 @@ describe "Dashboard access", feature: true do it { expect(new_group_path).to be_allowed_for :user } it { expect(new_group_path).to be_denied_for :visitor } end + + describe "GET /profile/groups" do + subject { dashboard_groups_path } + + 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 diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index 5f254c42e5..2512a9c0e3 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -1,76 +1,65 @@ require 'spec_helper' -describe "Users Security", feature: true do - describe "Project" do - before do - @u1 = create(:user) - end +describe "Profile access", feature: true do + before do + @u1 = create(:user) + end - describe "GET /login" do - it { expect(new_user_session_path).not_to be_404_for :visitor } - end + describe "GET /login" do + it { expect(new_user_session_path).not_to be_404_for :visitor } + end - describe "GET /profile/keys" do - subject { profile_keys_path } + describe "GET /profile/keys" do + subject { profile_keys_path } - it { is_expected.to be_allowed_for @u1 } - 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 + it { is_expected.to be_allowed_for @u1 } + 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 - describe "GET /profile" do - subject { profile_path } + describe "GET /profile" do + subject { profile_path } - it { is_expected.to be_allowed_for @u1 } - 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 + it { is_expected.to be_allowed_for @u1 } + 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 - describe "GET /profile/account" do - subject { profile_account_path } + describe "GET /profile/account" do + subject { profile_account_path } - it { is_expected.to be_allowed_for @u1 } - 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 + it { is_expected.to be_allowed_for @u1 } + 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 - describe "GET /profile/design" do - subject { design_profile_path } + describe "GET /profile/design" do + subject { design_profile_path } - it { is_expected.to be_allowed_for @u1 } - 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 + it { is_expected.to be_allowed_for @u1 } + 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 - describe "GET /profile/history" do - subject { history_profile_path } + describe "GET /profile/history" do + subject { history_profile_path } - it { is_expected.to be_allowed_for @u1 } - 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 + it { is_expected.to be_allowed_for @u1 } + 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 - describe "GET /profile/notifications" do - subject { profile_notifications_path } + describe "GET /profile/notifications" do + subject { profile_notifications_path } - it { is_expected.to be_allowed_for @u1 } - 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 - - describe "GET /profile/groups" do - subject { profile_groups_path } - - it { is_expected.to be_allowed_for @u1 } - 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 + it { is_expected.to be_allowed_for @u1 } + 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 diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index c4a192ac1a..b392371deb 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -4,6 +4,8 @@ describe EventsHelper do include ApplicationHelper include GitlabMarkdownHelper + let(:current_user) { create(:user, email: "current@email.com") } + it 'should display one line of plain text without alteration' do input = 'A short, plain note' expect(event_note(input)).to match(input) @@ -50,4 +52,14 @@ describe EventsHelper do expect(event_note(input)).to match(link_url) expect(event_note(input)).to match(expected_link_text) end + + it 'should preserve code color scheme' do + input = "```ruby\ndef test\n 'hello world'\nend\n```" + expected = '
' \
+      "def test\n" \
+      "  \'hello world\'\n" \
+      "end\n" \
+      '
' + expect(event_note(input)).to eq(expected) + end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 27f506d7f0..66474a6241 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -9,6 +9,7 @@ describe GitlabMarkdownHelper do let(:user) { create(:user, username: 'gfm') } let(:commit) { project.repository.commit } + let(:earlier_commit){ project.repository.commit("HEAD~2") } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:snippet) { create(:project_snippet, project: project) } @@ -53,6 +54,53 @@ describe GitlabMarkdownHelper do to have_selector('a.gfm.foo') end + describe "referencing a commit range" do + let(:expected) { namespace_project_compare_path(project.namespace, project, from: earlier_commit.id, to: commit.id) } + + it "should link using a full id" do + actual = "What happened in #{earlier_commit.id}...#{commit.id}" + expect(gfm(actual)).to match(expected) + end + + it "should link using a short id" do + actual = "What happened in #{earlier_commit.short_id}...#{commit.short_id}" + expected = namespace_project_compare_path(project.namespace, project, from: earlier_commit.short_id, to: commit.short_id) + expect(gfm(actual)).to match(expected) + end + + it "should link inclusively" do + actual = "What happened in #{earlier_commit.id}..#{commit.id}" + expected = namespace_project_compare_path(project.namespace, project, from: "#{earlier_commit.id}^", to: commit.id) + expect(gfm(actual)).to match(expected) + end + + it "should link with adjacent text" do + actual = "(see #{earlier_commit.id}...#{commit.id})" + expect(gfm(actual)).to match(expected) + end + + it "should keep whitespace intact" do + actual = "Changes #{earlier_commit.id}...#{commit.id} dramatically" + expected = /Changes #{earlier_commit.id}...#{commit.id}<\/a> dramatically/ + expect(gfm(actual)).to match(expected) + end + + it "should not link with an invalid id" do + actual = expected = "What happened in #{earlier_commit.id.reverse}...#{commit.id.reverse}" + expect(gfm(actual)).to eq(expected) + end + + it "should include a title attribute" do + actual = "What happened in #{earlier_commit.id}...#{commit.id}" + expect(gfm(actual)).to match(/title="Commits #{earlier_commit.id} through #{commit.id}"/) + end + + it "should include standard gfm classes" do + actual = "What happened in #{earlier_commit.id}...#{commit.id}" + expect(gfm(actual)).to match(/class="\s?gfm gfm-commit_range\s?"/) + end + end + describe "referencing a commit" do let(:expected) { namespace_project_commit_path(project.namespace, project, commit) } diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb new file mode 100644 index 0000000000..448cd0c688 --- /dev/null +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe 'Gitlab::NoteDataBuilder' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:data) { Gitlab::NoteDataBuilder.build(note, user) } + let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors + + before(:each) do + expect(data).to have_key(:object_attributes) + expect(data[:object_attributes]).to have_key(:url) + expect(data[:object_attributes][:url]).to eq(note_url) + expect(data[:object_kind]).to eq('note') + expect(data[:user]).to eq(user.hook_attrs) + end + + describe 'When asking for a note on commit' do + let(:note) { create(:note_on_commit) } + + it 'returns the note and commit-specific data' do + expect(data).to have_key(:commit) + end + end + + describe 'When asking for a note on commit diff' do + let(:note) { create(:note_on_commit_diff) } + + it 'returns the note and commit-specific data' do + expect(data).to have_key(:commit) + end + end + + describe 'When asking for a note on issue' do + let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) } + let(:note) { create(:note_on_issue, noteable_id: issue.id) } + + it 'returns the note and issue-specific data' do + expect(data).to have_key(:issue) + expect(data[:issue]).to eq(issue.hook_attrs) + end + end + + describe 'When asking for a note on merge request' do + let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + + it 'returns the note and merge request data' do + expect(data).to have_key(:merge_request) + expect(data[:merge_request]).to eq(merge_request.hook_attrs) + end + end + + describe 'When asking for a note on merge request diff' do + let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + + it 'returns the note and merge request diff data' do + expect(data).to have_key(:merge_request) + expect(data[:merge_request]).to eq(merge_request.hook_attrs) + end + end + + describe 'When asking for a note on project snippet' do + let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } + let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + + it 'returns the note and project snippet data' do + expect(data).to have_key(:snippet) + expect(data[:snippet]).to eq(snippet.hook_attrs) + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 226395d46d..484950b120 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -31,6 +31,11 @@ describe Gitlab::ReferenceExtractor do expect(subject.commits).to eq([{ project: nil, id: '98cf0ae3' }]) end + it 'extracts commit ranges' do + subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4', nil) + expect(subject.commit_ranges).to eq([{ project: nil, id: '98cf0ae3...98cf0ae4' }]) + end + it 'extracts multiple references and preserves their order' do subject.analyze('@me and @you both care about this', nil) expect(subject.users).to eq([ @@ -110,5 +115,19 @@ describe Gitlab::ReferenceExtractor do expect(extracted[0].sha).to eq(commit.sha) expect(extracted[0].message).to eq(commit.message) end + + it 'accesses valid commit ranges' do + commit = project.repository.commit('master') + earlier_commit = project.repository.commit('master~2') + + subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}", + project) + extracted = subject.commit_ranges_for(project) + expect(extracted.size).to eq(1) + expect(extracted[0][0].sha).to eq(earlier_commit.sha) + expect(extracted[0][0].message).to eq(earlier_commit.message) + expect(extracted[0][1].sha).to eq(commit.sha) + expect(extracted[0][1].message).to eq(commit.message) + end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 94b2fd5508..5153ed15af 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -16,4 +16,62 @@ describe Gitlab::UrlBuilder do expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" end end + + describe 'When asking for a note on commit' do + let(:note) { create(:note_on_commit) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + describe 'When asking for a note on commit diff' do + let(:note) { create(:note_on_commit_diff) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + describe 'When asking for a note on issue' do + let(:issue) { create(:issue) } + let(:note) { create(:note_on_issue, noteable_id: issue.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on merge request' do + let(:merge_request) { create(:merge_request) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on merge request diff' do + let(:merge_request) { create(:merge_request) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on project snippet' do + let(:snippet) { create(:project_snippet) } + let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index a5f2200197..d53f6e2793 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -5,6 +5,7 @@ describe Notify do include EmailSpec::Matchers include RepoHelpers + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } let(:gitlab_sender) { Gitlab.config.gitlab.email_from } let(:recipient) { create(:user, email: 'recipient@example.com') } let(:project) { create(:project) } @@ -23,7 +24,7 @@ describe Notify do shared_examples 'an email sent from GitLab' do it 'is sent from GitLab' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq('GitLab') + expect(sender.display_name).to eq(gitlab_sender_display_name) expect(sender.address).to eq(gitlab_sender) end end @@ -568,9 +569,10 @@ describe Notify do let(:user) { create(:user) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits) } - let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: commits.first, to: commits.last) } + let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base), to: Commit.new(compare.head)) } + let(:send_from_committer_email) { false } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, false, send_from_committer_email) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -583,7 +585,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{commits.length} new commits pushed to repository/ + is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ end it 'includes commits list' do @@ -597,6 +599,58 @@ describe Notify do it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end + + it 'doesn not contain the misleading footer' do + is_expected.not_to have_body_text /you are a member of/ + end + + context "when set to send from committer email if domain matches" do + + let(:send_from_committer_email) { true } + + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return("gitlab.corp.company.com") + end + + context "when the committer email domain is within the GitLab domain" do + + before do + user.update_attribute(:email, "user@company.com") + user.confirm! + end + + it "is sent from the committer email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(user.email) + end + end + + context "when the committer email domain is not completely within the GitLab domain" do + + before do + user.update_attribute(:email, "user@something.company.com") + user.confirm! + end + + it "is sent from the default email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(gitlab_sender) + end + end + + context "when the committer email domain is outside the GitLab domain" do + + before do + user.update_attribute(:email, "user@mpany.com") + user.confirm! + end + + it "is sent from the default email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(gitlab_sender) + end + end + end end describe 'email on push with a single commit' do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index cb43fdb7fc..d1027f64d1 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2,16 +2,17 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# default_branch_protection :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/asana_service_spec.rb b/spec/models/asana_service_spec.rb index 83e39f87f3..13c8d54a2a 100644 --- a/spec/models/asana_service_spec.rb +++ b/spec/models/asana_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb index ee7f780c8f..91730da1ee 100644 --- a/spec/models/project_services/assembla_service_spec.rb +++ b/spec/models/project_services/assembla_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildbox_service_spec.rb index 050363e14c..39d7df54cf 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildbox_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index b34e36bc94..73f68301a3 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index fe5d62b2f5..d44064bbe6 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 0cd255f08e..8bfb19e524 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index c474f4a2d9..959044dc72 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -2,16 +2,21 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # + require 'spec_helper' describe GitlabIssueTrackerService do diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb new file mode 100644 index 0000000000..8ab847e643 --- /dev/null +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -0,0 +1,217 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# + +require 'spec_helper' + +describe HipchatService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:hipchat) { HipchatService.new } + let(:user) { create(:user, username: 'username') } + let(:project) { create(:project, name: 'project') } + let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } + let(:project_name) { project.name_with_namespace.gsub(/\s/, '') } + + before(:each) do + hipchat.stub( + project_id: project.id, + project: project, + room: 123456, + server: 'https://hipchat.example.com', + token: 'verySecret' + ) + WebMock.stub_request(:post, api_url) + end + + context 'push events' do + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it "should call Hipchat API for push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + obj_attr = push_sample_data[:object_attributes] + branch = push_sample_data[:ref].gsub('refs/heads/', '') + expect(message).to include("#{user.name} pushed to branch " \ + "#{branch} of " \ + "#{project_name}") + end + end + + context 'tag_push events' do + let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) } + + it "should call Hipchat API for tag push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a tag push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + obj_attr = push_sample_data[:object_attributes] + expect(message).to eq("#{user.name} pushed new tag " \ + "test to " \ + "#{project_name}\n") + end + end + + context 'issue events' do + let(:issue) { create(:issue, title: 'Awesome issue', description: 'please fix') } + let(:issue_service) { Issues::CreateService.new(project, user) } + let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } + + it "should call Hipchat API for issue events" do + hipchat.execute(issues_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create an issue message" do + message = hipchat.send(:create_issue_message, issues_sample_data) + + obj_attr = issues_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "issue ##{obj_attr["iid"]} in " \ + "#{project_name}: " \ + "Awesome issue" \ + "
please fix
") + end + end + + context 'merge request events' do + let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) } + let(:merge_service) { MergeRequests::CreateService.new(project, user) } + let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } + + it "should call Hipchat API for merge requests events" do + hipchat.execute(merge_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a merge request message" do + message = hipchat.send(:create_merge_request_message, + merge_sample_data) + + obj_attr = merge_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "merge request ##{obj_attr["iid"]} in " \ + "#{project_name}: " \ + "Awesome merge request" \ + "
please fix
") + end + end + + context "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:snippet) { create(:project_snippet, project: project) } + let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") } + let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")} + let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") } + + it "should call Hipchat API for commit comment events" do + data = Gitlab::NoteDataBuilder.build(commit_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + commit_id = Commit.truncate_sha(data[:commit][:id]) + title = hipchat.send(:format_title, data[:commit][:message]) + + expect(message).to eq("#{user.name} commented on " \ + "commit #{commit_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
a comment on a commit
") + end + + it "should call Hipchat API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + merge_id = data[:merge_request]['iid'] + title = data[:merge_request]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "merge request ##{merge_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
merge request note
") + end + + it "should call Hipchat API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + hipchat.execute(data) + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + issue_id = data[:issue]['iid'] + title = data[:issue]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "issue ##{issue_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
issue note
") + end + + it "should call Hipchat API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + snippet_id = data[:snippet]['id'] + title = data[:snippet]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "snippet ##{snippet_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
snippet note
") + end + end + end +end diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index bbd5245ad3..d55399bc36 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index f1d8048b26..6ebff9f21b 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index 188626a7a2..5a18fd09bf 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb new file mode 100644 index 0000000000..8bca1fef44 --- /dev/null +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe SlackService::IssueMessage do + subject { SlackService::IssueMessage.new(args) } + + let(:args) { + { + user: { + name: 'Test User', + username: 'Test User' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + action: 'open', + state: 'opened', + description: 'issue description' + } + } + } + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of issues' do + expect(subject.pretext).to eq( + 'Test User opened in : '\ + '*Issue title*') + expect(subject.attachments).to eq([ + { + text: "issue description", + color: color, + } + ]) + end + end + + context 'close' do + before do + args[:object_attributes][:action] = 'close' + args[:object_attributes][:state] = 'closed' + end + it 'returns a message regarding closing of issues' do + expect(subject.pretext). to eq( + 'Test User closed in : '\ + '*Issue title*') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb new file mode 100644 index 0000000000..aeb408aa76 --- /dev/null +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe SlackService::MergeMessage do + subject { SlackService::MergeMessage.new(args) } + + let(:args) { + { + user: { + name: 'Test User', + username: 'Test User' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: "Issue title\nSecond line", + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + state: 'opened', + description: 'issue description', + source_branch: 'source_branch', + target_branch: 'target_branch', + } + } + } + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of merge requests' do + expect(subject.pretext).to eq( + 'Test User opened '\ + 'in : *Issue title*') + expect(subject.attachments).to be_empty + end + end + + context 'close' do + before do + args[:object_attributes][:state] = 'closed' + end + it 'returns a message regarding closing of merge requests' do + expect(subject.pretext).to eq( + 'Test User closed '\ + 'in : *Issue title*') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb new file mode 100644 index 0000000000..21fb575480 --- /dev/null +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe SlackService::NoteMessage do + let(:color) { '#345' } + + before do + @args = { + user: { + name: 'Test User', + username: 'username', + avatar_url: 'http://fakeavatar' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + repository: { + name: 'project_name', + url: 'somewhere.com', + }, + object_attributes: { + id: 10, + note: 'comment on a commit', + url: 'url', + noteable_type: 'Commit' + } + } + end + + context 'commit notes' do + before do + @args[:object_attributes][:note] = 'comment on a commit' + @args[:object_attributes][:noteable_type] = 'Commit' + @args[:commit] = { + id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23', + message: "Added a commit message\ndetails\n123\n" + } + end + + it 'returns a message regarding notes on commits' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + " in : " \ + "*Added a commit message*") + expected_attachments = [ + { + text: "comment on a commit", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'merge request notes' do + before do + @args[:object_attributes][:note] = 'comment on a merge request' + @args[:object_attributes][:noteable_type] = 'MergeRequest' + @args[:merge_request] = { + id: 1, + iid: 30, + title: "merge request title\ndetails\n" + } + end + it 'returns a message regarding notes on a merge request' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + " in : " \ + "*merge request title*") + expected_attachments = [ + { + text: "comment on a merge request", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'issue notes' do + before do + @args[:object_attributes][:note] = 'comment on an issue' + @args[:object_attributes][:noteable_type] = 'Issue' + @args[:issue] = { + id: 1, + iid: 20, + title: "issue title\ndetails\n" + } + end + + it 'returns a message regarding notes on an issue' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq( + "Test User commented on " \ + " in : " \ + "*issue title*") + expected_attachments = [ + { + text: "comment on an issue", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'project snippet notes' do + before do + @args[:object_attributes][:note] = 'comment on a snippet' + @args[:object_attributes][:noteable_type] = 'Snippet' + @args[:snippet] = { + id: 5, + title: "snippet title\ndetails\n" + } + end + + it 'returns a message regarding notes on a project snippet' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + " in : " \ + "*snippet title*") + expected_attachments = [ + { + text: "comment on a snippet", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end +end diff --git a/spec/models/project_services/slack_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb similarity index 64% rename from spec/models/project_services/slack_message_spec.rb rename to spec/models/project_services/slack_service/push_message_spec.rb index 7197a94e53..10963481a1 100644 --- a/spec/models/project_services/slack_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackMessage do - subject { SlackMessage.new(args) } +describe SlackService::PushMessage do + subject { SlackService::PushMessage.new(args) } let(:args) { { @@ -31,17 +31,37 @@ describe SlackMessage do ) expect(subject.attachments).to eq([ { - text: ": message1 - author1\n"\ - ": message2 - author2", + text: ": message1 - author1\n"\ + ": message2 - author2", color: color, } ]) end end + context 'tag push' do + let(:args) { + { + after: 'after', + before: Gitlab::Git::BLANK_SHA, + project_name: 'project_name', + ref: 'refs/tags/new_tag', + user_name: 'user_name', + project_url: 'url' + } + } + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq('user_name pushed new tag ' \ + ' to ' \ + '') + expect(subject.attachments).to be_empty + end + end + context 'new branch' do before do - args[:before] = '000000' + args[:before] = Gitlab::Git::BLANK_SHA end it 'returns a message regarding a new branch' do @@ -55,7 +75,7 @@ describe SlackMessage do context 'removed branch' do before do - args[:after] = '000000' + args[:after] = Gitlab::Git::BLANK_SHA end it 'returns a message regarding a removed branch' do diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 8a75d8987a..c36506644b 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' @@ -34,7 +39,7 @@ describe SlackService do let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project) } - let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } let(:username) { 'slack_username' } let(:channel) { 'slack_channel' } @@ -48,10 +53,43 @@ describe SlackService do ) WebMock.stub_request(:post, webhook_url) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'stable', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') end - it "should call Slack API" do - slack.execute(sample_data) + it "should call Slack API for push events" do + slack.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for issue events" do + slack.execute(@issues_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for merge requests events" do + slack.execute(@merge_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -59,11 +97,11 @@ describe SlackService do it 'should use the username as an option for slack when configured' do slack.stub(username: username) expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). - and_return( - double(:slack_service).as_null_object - ) - slack.execute(sample_data) + with(webhook_url, username: username). + and_return( + double(:slack_service).as_null_object + ) + slack.execute(push_sample_data) end it 'should use the channel as an option when it is configured' do @@ -73,7 +111,60 @@ describe SlackService do and_return( double(:slack_service).as_null_object ) - slack.execute(sample_data) + slack.execute(push_sample_data) + end + end + + describe "Note events" do + let(:slack) { SlackService.new } + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:snippet) { create(:project_snippet, project: project) } + let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") } + let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")} + let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") } + let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } + + before do + slack.stub( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + it "should call Slack API for commit comment events" do + data = Gitlab::NoteDataBuilder.build(commit_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index eeb0f3d9ee..b3a38f6c5b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -18,4 +18,27 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end + + context :timestamps_by_user_log do + before do + Date.stub(:today).and_return(Date.new(2015, 03, 01)) + end + + describe 'single e-mail for user' do + let(:user) { create(:user, email: sample_commit.author_email) } + + subject { repository.timestamps_by_user_log(user) } + + it { is_expected.to eq(["2014-08-06", "2014-07-31", "2014-07-31"]) } + end + + describe 'multiple emails for user' do + let(:email_alias) { create(:email, email: another_sample_commit.author_email) } + let(:user) { create(:user, email: sample_commit.author_email, emails: [email_alias]) } + + subject { repository.timestamps_by_user_log(user) } + + it { is_expected.to eq(["2015-01-10", "2014-08-06", "2014-07-31", "2014-07-31"]) } + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 9fe3f8cfb1..633ead05b2 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -2,15 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b31acde606..ac20e0e817 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,47 +2,53 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# last_credential_check_at :datetime -# github_access_token :string(255) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# github_access_token :string(255) +# gitlab_access_token :string(255) +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# bitbucket_access_token :string(255) +# bitbucket_access_token_secret :string(255) # require 'spec_helper' diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 5e2cb3c614..78bac58cd7 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -49,6 +49,7 @@ describe GitPushService do subject { @push_data } + it { is_expected.to include(object_kind: 'push') } it { is_expected.to include(before: @oldrev) } it { is_expected.to include(after: @newrev) } it { is_expected.to include(ref: @ref) } @@ -196,15 +197,6 @@ describe GitPushService do service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') end - - it "finds references in the first push to a default branch" do - allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([]) - allow(project.repository).to receive(:commits).with(@newrev).and_return([commit]) - - expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project) - - service.execute(project, user, @blankrev, @newrev, 'refs/heads/master') - end end describe "closing issues from pushed commits" do diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 504213e667..a97c55011c 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -21,10 +21,8 @@ describe Issues::BulkUpdateService do create(:issue, project: @project) end @params = { - update: { - status: 'closed', - issues_ids: @issues.map(&:id) - } + state_event: 'close', + issues_ids: @issues.map(&:id) } end @@ -46,10 +44,8 @@ describe Issues::BulkUpdateService do create(:closed_issue, project: @project) end @params = { - update: { - status: 'reopen', - issues_ids: @issues.map(&:id) - } + state_event: 'reopen', + issues_ids: @issues.map(&:id) } end @@ -69,10 +65,8 @@ describe Issues::BulkUpdateService do before do @new_assignee = create :user @params = { - update: { - issues_ids: [issue.id], - assignee_id: @new_assignee.id - } + issues_ids: [issue.id], + assignee_id: @new_assignee.id } end @@ -88,7 +82,7 @@ describe Issues::BulkUpdateService do @project.issues.first.update_attribute(:assignee, @new_assignee) expect(@project.issues.first.assignee).not_to be_nil - @params[:update][:assignee_id] = -1 + @params[:assignee_id] = -1 Issues::BulkUpdateService.new(@project, @user, @params).execute expect(@project.issues.first.assignee).to be_nil @@ -98,7 +92,7 @@ describe Issues::BulkUpdateService do @project.issues.first.update_attribute(:assignee, @new_assignee) expect(@project.issues.first.assignee).not_to be_nil - @params[:update][:assignee_id] = '' + @params[:assignee_id] = '' Issues::BulkUpdateService.new(@project, @user, @params).execute expect(@project.issues.first.assignee).not_to be_nil @@ -110,10 +104,8 @@ describe Issues::BulkUpdateService do before do @milestone = create :milestone @params = { - update: { - issues_ids: [issue.id], - milestone_id: @milestone.id - } + issues_ids: [issue.id], + milestone_id: @milestone.id } end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2830da8781..879df0c9c6 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -61,7 +61,7 @@ describe MergeRequests::RefreshService do it { expect(@merge_request.notes).to be_empty } it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('new commit') } + it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') } it { expect(@fork_merge_request).to be_open } end diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index 4c4775da69..aadf791bf3 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -43,6 +43,25 @@ eos ) end + def another_sample_commit + OpenStruct.new( + id: "e56497bb5f03a90a51293fc6d516788730953899", + parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40', + author_full_name: "Sytse Sijbrandij", + author_email: "sytse@gitlab.com", + files_changed_count: 1, + message: <