diff --git a/.pkgr.yml b/.pkgr.yml new file mode 100644 index 0000000000..09cb83783d --- /dev/null +++ b/.pkgr.yml @@ -0,0 +1,19 @@ +user: git +group: git +before_precompile: ./bin/pkgr_before_precompile.sh +targets: + debian-7: &wheezy + build_dependencies: + - libicu-dev + dependencies: + - libicu48 + - libpcre3 + - git + ubuntu-12.04: *wheezy + ubuntu-14.04: + build_dependencies: + - libicu-dev + dependencies: + - libicu52 + - libpcre3 + - git diff --git a/CHANGELOG b/CHANGELOG index 185e5df46c..61c6b94b79 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,18 @@ +v 6.9.0 + - Store Rails cache data in the Redis `cache:gitlab` namespace + - Adjust MySQL limits for existing installations + - Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed) + - Markdown preview or diff during editing via web editor (Evgeniy Sokovikov) + - Give the Rails cache its own Redis namespace + - Add ability to set different ssh host, if different from http/https + - Fix syntax highlighting for code comments blocks + - Improve comments loading logic + - Stop refreshing comments when the tab is hidden + - Improve issue and merge request mobile UI (Drew Blessing) + - Document how to convert a backup to PostgreSQL + - Fix locale bug in backup manager + - Fix wiki backup skip bug + v 6.8.1 - Bump required gitlab-shell version to 1.9.3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 290804e618..780db547f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,9 +107,10 @@ For examples of feedback on merge requests please look at already [closed merge ## Style guides 1. [Ruby](https://github.com/bbatsov/ruby-style-guide) -2. [Rails](https://github.com/bbatsov/rails-style-guide) -3. [Formatting](https://github.com/thoughtbot/guides/tree/master/style#formatting) -4. [Naming](https://github.com/thoughtbot/guides/tree/master/style#naming) -8. [Testing](https://github.com/thoughtbot/guides/tree/master/style#testing) -7. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) -9. [Shell commands](doc/development/shell_commands.md) +1. [Rails](https://github.com/bbatsov/rails-style-guide) +1. [Formatting](https://github.com/thoughtbot/guides/tree/master/style#formatting) +1. [Naming](https://github.com/thoughtbot/guides/tree/master/style#naming) +1. [Testing](https://github.com/thoughtbot/guides/tree/master/style#testing) +1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) +1. [Shell commands](doc/development/shell_commands.md) +1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) diff --git a/Gemfile b/Gemfile index 07b244b52a..51418f57d4 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem "carrierwave" # for aws storage gem "fog", "~> 1.14", group: :aws +gem "unf", group: :aws # Authorization gem "six" @@ -83,6 +84,9 @@ gem "seed-fu" gem "redcarpet", "~> 2.2.2" gem "github-markup" +# Diffs +gem 'diffy', '~> 3.0.3' + # Asciidoc to HTML gem "asciidoctor" @@ -158,8 +162,8 @@ gem 'jquery-turbolinks' gem 'select2-rails' gem 'jquery-atwho-rails', "~> 0.3.3" -gem "jquery-rails", "2.1.3" -gem "jquery-ui-rails", "2.0.2" +gem "jquery-rails" +gem "jquery-ui-rails" gem "raphael-rails", "~> 2.1.2" gem 'bootstrap-sass', '~> 3.0' gem "font-awesome-rails", '~> 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index d0f9f15d1d..07a611a2fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,7 @@ GEM devise-async (0.8.0) devise (>= 2.2, < 3.2) diff-lcs (1.2.5) + diffy (3.0.3) docile (1.1.1) dotenv (0.9.0) email_spec (1.5.0) @@ -161,7 +162,7 @@ GEM multi_json gitlab-grack (2.0.0.pre) rack (~> 1.5.1) - gitlab-grit (2.6.5) + gitlab-grit (2.6.6) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) mime-types (~> 1.15) @@ -246,15 +247,14 @@ GEM rake jasmine-core (2.0.0.rc5) jquery-atwho-rails (0.3.3) - jquery-rails (2.1.3) - railties (>= 3.1.0, < 5.0) - thor (~> 0.14) + jquery-rails (3.1.0) + railties (>= 3.0, < 5.0) + thor (>= 0.14, < 2.0) jquery-turbolinks (2.0.1) railties (>= 3.1.0) turbolinks - jquery-ui-rails (2.0.2) - jquery-rails - railties (>= 3.1.0) + jquery-ui-rails (4.2.1) + railties (>= 3.2.16) json (1.8.1) jwt (0.1.8) multi_json (>= 1.5) @@ -530,6 +530,9 @@ GEM execjs (>= 0.3.0) json (>= 1.8.0) underscore-rails (1.4.4) + unf (0.1.4) + unf_ext + unf_ext (0.0.6) unicorn (4.6.3) kgio (~> 2.6) rack @@ -573,6 +576,7 @@ DEPENDENCIES default_value_for (~> 3.0.0) devise (= 3.0.4) devise-async (= 0.8.0) + diffy (~> 3.0.3) email_spec email_validator (~> 1.4.0) enumerize @@ -602,9 +606,9 @@ DEPENDENCIES httparty jasmine (= 2.0.0.rc5) jquery-atwho-rails (~> 0.3.3) - jquery-rails (= 2.1.3) + jquery-rails jquery-turbolinks - jquery-ui-rails (= 2.0.2) + jquery-ui-rails kaminari (~> 0.15.1) launchy letter_opener @@ -659,6 +663,7 @@ DEPENDENCIES turbolinks uglifier underscore-rails (~> 1.4.4) + unf unicorn (~> 4.6.3) unicorn-worker-killer version_sorter diff --git a/README.md b/README.md index 3f3453a267..c7c979c0ca 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,11 @@ * [GitLab.com](https://www.gitlab.com/) includes information about [subscriptions](https://www.gitlab.com/subscription/), [consultancy](https://www.gitlab.com/consultancy/), the [community](https://www.gitlab.com/community/) and the [hosted GitLab Cloud](https://www.gitlab.com/cloud/). -* [GitLab Enterprise Edition](https://www.gitlab.com/gitlab-ce/) offers additional features that are useful for larger organizations (100+ users). +* [GitLab Enterprise Edition](https://www.gitlab.com/gitlab-ee/) offers additional features aimed at larger organizations. * [GitLab CI](https://www.gitlab.com/gitlab-ci/) is a continuous integration (CI) server that is easy to integrate with GitLab. -* Unofficial third-party [iPhone app](http://gitlabcontrol.com/) and [Android app](https://play.google.com/store/apps/details?id=com.bd.gitlab&hl=en) for GitLab +* Unofficial third-party [iPhone app](http://gitlabcontrol.com/), [Android app](https://play.google.com/store/apps/details?id=com.bd.gitlab&hl=en) and [command line client](https://github.com/drewblessing/gitlab-cli) for GitLab. ### Requirements diff --git a/VERSION b/VERSION index 4dd7f4527b..13530c8855 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.8.0-ee +6.9.0.pre-ee diff --git a/app/assets/images/ui-icons_222222_256x240.png b/app/assets/images/ui-icons_222222_256x240.png deleted file mode 100644 index 8bc06cbf03..0000000000 Binary files a/app/assets/images/ui-icons_222222_256x240.png and /dev/null differ diff --git a/app/assets/images/ui-icons_454545_256x240.png b/app/assets/images/ui-icons_454545_256x240.png deleted file mode 100644 index cfd1eaffaa..0000000000 Binary files a/app/assets/images/ui-icons_454545_256x240.png and /dev/null differ diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 46d6db0f05..b61d9875e0 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -59,7 +59,7 @@ class Dispatcher initHighlight: -> $('.highlight pre code').each (i, e) -> - hljs.highlightBlock(e) $(e).html($.map($(e).html().split("\n"), (line, i) -> - "
" + line + "
" + "" + line + "" ).join("\n")) + hljs.highlightBlock(e) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index d200d962ca..47989010d1 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,10 +1,11 @@ class Notes @interval: null - constructor: (notes_url, note_ids) -> + constructor: (notes_url, note_ids, last_fetched_at) -> @notes_url = notes_url @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root? @note_ids = note_ids + @last_fetched_at = last_fetched_at @initRefresh() @setupMainTargetNoteForm() @cleanBinding() @@ -49,6 +50,15 @@ class Notes # hide diff note form $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm + # fetch notes when tab becomes visible + $(document).on "visibilitychange", @visibilityChange + + @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' + $(document).on('keypress', @notes_forms, (e)-> + if event.keyCode == 10 || (event.ctrlKey && event.keyCode == 13) + $(@).parents('form').submit() + ) + cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" @@ -62,6 +72,8 @@ class Notes $(document).off "click", ".js-choose-note-attachment-button" $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" + $(document).off "visibilitychange" + $(document).off "keypress", @notes_forms initRefresh: -> @@ -71,14 +83,16 @@ class Notes , 15000 refresh: -> - @getContent() + @getContent() unless document.hidden getContent: -> $.ajax url: @notes_url + data: "last_fetched_at=" + @last_fetched_at dataType: "json" success: (data) => notes = data.notes + @last_fetched_at = data.last_fetched_at $.each notes, (i, note) => @renderNote(note) @@ -450,4 +464,10 @@ class Notes filename = $(this).val().replace(/^.*[\\\/]/, "") form.find(".js-attachment-filename").text filename + ### + Called when the tab visibility changes + ### + visibilityChange: => + @refresh() + @Notes = Notes diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee index 03fad41c49..b0e39610fe 100644 --- a/app/assets/javascripts/project_users_select.js.coffee +++ b/app/assets/javascripts/project_users_select.js.coffee @@ -1,7 +1,7 @@ @projectUsersSelect = init: -> $('.ajax-project-users-select').each (i, select) -> - project_id = $('body').data('project-id') + project_id = $(select).data('project-id') || $('body').data('project-id') $(select).select2 placeholder: $(select).data('placeholder') || "Search for a user" diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 2ce09a6553..c53873f95a 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -12,10 +12,7 @@ *= require nprogress-bootstrap */ -@import "main/variables.scss"; -@import "main/mixins.scss"; -@import "main/fonts.scss"; -@import "main/layout.scss"; +@import "main/*"; /** * Customized Twitter bootstrap @@ -31,65 +28,22 @@ /** * Generic css (forms, nav etc): */ -@import "generic/avatar.scss"; -@import "generic/common.scss"; -@import "generic/typography.scss"; -@import "generic/buttons.scss"; -@import "generic/blocks.scss"; -@import "generic/ui_box.scss"; -@import "generic/issue_box.scss"; -@import "generic/files.scss"; -@import "generic/lists.scss"; -@import "generic/flash.scss"; -@import "generic/forms.scss"; -@import "generic/selects.scss"; -@import "generic/highlight.scss"; -@import "generic/jquery.scss"; +@import "generic/*"; /** * Page specific styles (issues, projects etc): */ -@import "sections/header.scss"; -@import "sections/nav.scss"; -@import "sections/commits.scss"; -@import "sections/diff.scss"; -@import "sections/issues.scss"; -@import "sections/projects.scss"; -@import "sections/snippets.scss"; -@import "sections/votes.scss"; -@import "sections/merge_requests.scss"; -@import "sections/graph.scss"; -@import "sections/events.scss"; -@import "sections/themes.scss"; -@import "sections/tree.scss"; -@import "sections/notes.scss"; -@import "sections/profile.scss"; -@import "sections/login.scss"; -@import "sections/editor.scss"; -@import "sections/admin.scss"; -@import "sections/wiki.scss"; -@import "sections/wall.scss"; -@import "sections/dashboard.scss"; -@import "sections/stat_graph.scss"; -@import "sections/groups.scss"; -@import "sections/appearances.scss"; +@import "sections/*"; /** * Code highlight */ -@import "highlight/white.scss"; -@import "highlight/dark.scss"; -@import "highlight/solarized_dark.scss"; -@import "highlight/monokai.scss"; +@import "highlight/*"; /** * UI themes: */ -@import "themes/ui_basic.scss"; -@import "themes/ui_mars.scss"; -@import "themes/ui_modern.scss"; -@import "themes/ui_gray.scss"; -@import "themes/ui_color.scss"; +@import "themes/*"; /** * Styles for JS behaviors. diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 12559f7605..6418f24d97 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -26,6 +26,10 @@ margin-top: -5px; } + .left-options { + margin-top: -3px; + } + .file_name { color: $style_color; font-size: 14px; diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss index 3db4d908d9..d4d3361bc7 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/generic/issue_box.scss @@ -12,6 +12,7 @@ margin:20px 0; background: #FFF; border: 1px solid #EEE; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); &.issue-box-closed { border-color: #DA4E49; @@ -70,7 +71,6 @@ } .state { - height: 34px; border-bottom: 1px solid #DDD; line-height: 32px; } @@ -89,6 +89,18 @@ border: none; border-top: 1px solid #eee; padding: 15px 25px; + + // Reset text align for children + .text-right > * { text-align: left; } + + @media (max-width: $screen-xs-max) { + // Don't right align on mobile + .text-right { text-align: left; } + + .row .col-md-6 { + padding-top: 5px; + } + } } .description { @@ -106,7 +118,11 @@ padding: 1px 25px; text-align: center; text-shadow: none; - margin-right: 20px; display: inline-block; + line-height: 34px; + } + + .creator { + padding: 2px 15px; } } diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/generic/jquery.scss index 4a9341e8f5..6b29accb31 100644 --- a/app/assets/stylesheets/generic/jquery.scss +++ b/app/assets/stylesheets/generic/jquery.scss @@ -8,7 +8,7 @@ width: 270px; .ui-datepicker-header { - background: #EEE; + background: #FFF; border-color: #DDD; } @@ -19,20 +19,37 @@ } &.ui-autocomplete { - @include border-radius(0px); border-color: #DDD; padding: 0; + margin-top: 2px; + z-index: 1001; .ui-menu-item a { - color: #777; - - &:hover { - background: $hover; - border-color: $primary_color; - @include border-radius(0px); - color: #333; - } + padding: 4px 10px; } } -} + .ui-state-default { + border: 1px solid #FFF; + background: #FFF; + color: #777; + } + + .ui-state-highlight { + border: 1px solid #EEE; + background: #EEE; + } + + .ui-state-active { + border: 1px solid $bg_style_color; + background: $bg_style_color; + color: #FFF; + } + + .ui-state-hover, + .ui-state-focus { + border: 1px solid $hover; + background: $hover; + color: #333; + } +} diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index a441955173..8cc72d7f07 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -47,7 +47,7 @@ a { text-decoration: underline; } - &.dark { + &.darken { color: $style_color; } diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/main/mixins.scss index fcc7374d0d..147c759ecb 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/main/mixins.scss @@ -41,31 +41,6 @@ * Prefilled mixins * Mixins with fixed values */ -@mixin bg-light-gray-gradient { - background: #f1f1f1; - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #f5f5f5), to(#e1e1e1)); - background-image: -webkit-linear-gradient(#f5f5f5 6.6%, #e1e1e1); - background-image: -moz-linear-gradient(#f5f5f5 6.6%, #e1e1e1); - background-image: -ms-linear-gradient(#f5f5f5 6.6%, #e1e1e1); - background-image: -o-linear-gradient(#f5f5f5 6.6%, #e1e1e1); -} - -@mixin bg-gray-gradient { - background: #eee; - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); - background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -ms-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); -} - -@mixin bg-dark-gray-gradient { - background: #eee; - background-image: -webkit-linear-gradient(#e9e9e9, #d7d7d7); - background-image: -moz-linear-gradient(#e9e9e9, #d7d7d7); - background-image: -ms-linear-gradient(#e9e9e9, #d7d7d7); - background-image: -o-linear-gradient(#e9e9e9, #d7d7d7); -} @mixin shade { @include box-shadow(0 0 3px #ddd); diff --git a/app/assets/stylesheets/sections/diff.scss b/app/assets/stylesheets/sections/diff.scss index eb272f20f4..64e669ac2b 100644 --- a/app/assets/stylesheets/sections/diff.scss +++ b/app/assets/stylesheets/sections/diff.scss @@ -63,26 +63,14 @@ } } - .text-file-parallel div { - display: inline-block; - padding-bottom: 16px; - } - .diff-side { - overflow-x: scroll; - width: 508px; - height: 700px; - } - .diff-side.diff-side-left{ - overflow-y:hidden; - } - .diff-side table, td.diff-middle table { - height: 700px; - } - .diff-middle { - width: 114px; - vertical-align: top; - height: 700px; - overflow: hidden + tr.line_holder.parallel{ + .old_line, .new_line, .diff_line { + min-width: 50px; + } + + td.line_content.parallel{ + width: 50%; + } } .old_line, .new_line, .diff_line { diff --git a/app/assets/stylesheets/sections/graph.scss b/app/assets/stylesheets/sections/graph.scss index 1e22d161bf..8a337a5e20 100644 --- a/app/assets/stylesheets/sections/graph.scss +++ b/app/assets/stylesheets/sections/graph.scss @@ -1,17 +1,16 @@ .project-network { - border: 1px solid #aaa; - padding: 1px; + border: 1px solid #CCC; .tip { color: #888; font-size: 14px; padding: 10px; border-bottom: 1px solid #bbb; - @include bg-gray-gradient; + background: #EEE; } .network-graph { - background: #f1f1f1; + background: #FFF; height: 500px; overflow-y: scroll; overflow-x: hidden; diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index d4f8c8108a..02c9123178 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -45,14 +45,6 @@ padding: 6px 10px; border: 1px solid #ccc; @include border-radius(4px); - - - input.check_all_issues { - padding: 0; - margin: 0; - position: relative; - top: 3px; - } } .issues_content { @@ -143,3 +135,36 @@ form.edit-issue { border-color: #E5E5E5; } } + +@media (max-width: $screen-xs-max) { + .issue-btn-group { + width: 100%; + margin-top: 5px; + + .btn-group { + width: 100%; + + ul { + width: 100%; + text-align: center; + } + } + + .btn { + width: 100%; + margin-top: -1px; + + &:first-child:not(:last-child) { + border-radius: 4px 4px 0 0; + } + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + + &:last-child:not(:first-child) { + border-radius: 0 0 4px 4px; + } + } + } +} diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 790496a1a5..5dcfc449b4 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -31,7 +31,6 @@ .mr_source_commit, .mr_target_commit { - margin-top: 10px; .commit { margin: 0; padding: 2px 0; diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index c9c7b6ecce..7e56781f56 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -139,6 +139,7 @@ ul.notes { background-color: #fff; border-width: 1px 0; padding-top: 0; + vertical-align: top; li { padding: 5px; diff --git a/app/assets/stylesheets/sections/votes.scss b/app/assets/stylesheets/sections/votes.scss index 13f811e01a..d683e33e1f 100644 --- a/app/assets/stylesheets/sections/votes.scss +++ b/app/assets/stylesheets/sections/votes.scss @@ -40,4 +40,10 @@ .votes-holder { float: right; width: 250px; + + @media (max-width: $screen-xs-max) { + width: 100%; + margin-top: 5px; + margin-bottom: 10px; + } } diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 13a7bdcf34..92ef596337 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -12,6 +12,7 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.search(params[:name]) if params[:name].present? + @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20) end diff --git a/app/controllers/projects/edit_tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb index ff5206b6fa..be611892bb 100644 --- a/app/controllers/projects/edit_tree_controller.rb +++ b/app/controllers/projects/edit_tree_controller.rb @@ -26,6 +26,18 @@ class Projects::EditTreeController < Projects::BaseTreeController end end + def preview + @content = params[:content] + #FIXME workaround https://github.com/gitlabhq/gitlabhq/issues/5936 + @content += "\n" if @blob.data.end_with?("\n") + + diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', + include_diff_info: true) + @diff = Gitlab::DiffParser.new(diffy.diff.scan(/.*\n/)) + + render layout: false + end + private def blob diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 745da9c49e..d8551db7b0 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -62,11 +62,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.source_project = @project unless @merge_request.source_project @merge_request.target_project ||= (@project.forked_from_project || @project) @target_branches = @merge_request.target_project.nil? ? [] : @merge_request.target_project.repository.branch_names - @merge_request.target_branch ||= @merge_request.target_project.default_branch - @source_project = @merge_request.source_project - @merge_request + + if @merge_request.target_branch && @merge_request.source_branch + compare_action = Gitlab::Satellite::CompareAction.new( + current_user, + @merge_request.target_project, + @merge_request.target_branch, + @merge_request.source_project, + @merge_request.source_branch + ) + + @commits = compare_action.commits + @commits.map! { |commit| Commit.new(commit) } + @commit = @commits.first + + @diffs = compare_action.diffs + @merge_request.title = @merge_request.source_branch.titleize.humanize + @target_project = @merge_request.target_project + @target_repo = @target_project.repository + end end def edit @@ -80,7 +96,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request = MergeRequests::CreateService.new(project, current_user, params[:merge_request]).execute if @merge_request.valid? - redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully created.' + redirect_to project_merge_request_path(@merge_request.target_project, @merge_request), notice: 'Merge request was successfully created.' else @source_project = @merge_request.source_project @target_project = @merge_request.target_project diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 85d042a89b..b5b0446b43 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -5,9 +5,10 @@ class Projects::NotesController < Projects::ApplicationController before_filter :authorize_admin_note!, only: [:update, :destroy] def index + current_fetched_at = Time.now.to_i @notes = NotesFinder.new.execute(project, current_user, params) - notes_json = { notes: [] } + notes_json = { notes: [], last_fetched_at: current_fetched_at } @notes.each do |note| notes_json[:notes] << { diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index bcd9e0d521..496064c9a6 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -12,9 +12,22 @@ class Projects::WikisController < Projects::ApplicationController def show @page = @project_wiki.find_page(params[:id], params[:version_id]) + gollum_wiki = @project_wiki.wiki + file = gollum_wiki.file(params[:id], gollum_wiki.ref, true) if @page render 'show' + elsif file + if file.on_disk? + send_file file.on_disk_path, disposition: 'inline' + else + send_data( + file.raw_data, + type: file.mime_type, + disposition: 'inline', + filename: file.name + ) + end else return render('empty') unless can?(current_user, :write_wiki, @project) @page = WikiPage.new(@project_wiki) diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 384316e14b..ea055694cd 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,9 +1,13 @@ class NotesFinder + FETCH_OVERLAP = 5.seconds + def execute(project, current_user, params) target_type = params[:target_type] target_id = params[:target_id] + # Default to 0 to remain compatible with old clients + last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) - case target_type + notes = case target_type when "commit" project.notes.for_commit_id(target_id).not_inline.fresh when "issue" @@ -12,6 +16,11 @@ class NotesFinder project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh when "snippet" project.snippets.find(target_id).notes.fresh + else + raise 'invalid target_type' end + + # Use overlapping intervals to avoid worrying about race conditions + notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 00ee734705..d8ba2727cc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -75,7 +75,7 @@ module ApplicationHelper else gravatar_url = request.ssl? || gitlab_config.https ? Gitlab.config.gravatar.ssl_url : Gitlab.config.gravatar.plain_url user_email.strip! - sprintf gravatar_url, hash: Digest::MD5.hexdigest(user_email.downcase), size: size + sprintf gravatar_url, hash: Digest::MD5.hexdigest(user_email.downcase), size: size, email: user_email end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index c6e4f574b6..c4abdbdabc 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -16,9 +16,10 @@ module CommitsHelper end def each_diff_line(diff, index) - Gitlab::DiffParser.new(diff).each do |full_line, type, line_code, line_new, line_old| - yield(full_line, type, line_code, line_new, line_old) - end + Gitlab::DiffParser.new(diff.diff.lines.to_a, diff.new_path) + .each do |full_line, type, line_code, line_new, line_old| + yield(full_line, type, line_code, line_new, line_old) + end end def each_diff_line_near(diff, index, expected_line_code) @@ -116,7 +117,7 @@ module CommitsHelper added_lines[line_new] = { line_code: line_code, type: type, line: line } end end - max_length = old_file ? old_file.sloc + added_lines.length : file.sloc + max_length = old_file ? [old_file.loc, file.loc].max : file.loc offset1 = 0 offset2 = 0 diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 95f0eff58b..7c58908165 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -82,7 +82,7 @@ module IssuesHelper end def milestone_options object - options_from_collection_for_select(@project.milestones.active, 'id', 'title', object.milestone_id) + options_from_collection_for_select(object.project.milestones.active, 'id', 'title', object.milestone_id) end def issue_box_class(item) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9bb3efc41d..ef0460f872 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -163,7 +163,7 @@ module ProjectsHelper end def repository_size(project = nil) - "#{(project || @project).repository.size} MB" + "#{(project || @project).repository_size} MB" rescue # In order to prevent 500 error # when application cannot allocate memory diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index a1fe4488ae..ab24367c45 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -14,7 +14,7 @@ module SelectsHelper css_class << (opts[:class] || '') value = opts[:selected] || '' placeholder = opts[:placeholder] || 'Select user' - - hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder) + project_id = opts[:project_id] || @project.id + hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder, 'data-project-id' => project_id) end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 50501dffef..f39d0081dc 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -91,4 +91,12 @@ module TreeHelper def leave_edit_message "Leave edit mode?\nAll unsaved changes will be lost." end + + def editing_preview_title(filename) + if gitlab_markdown?(filename) || markup?(filename) + 'Preview' + else + 'Diff' + end + end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index a97d55f1b5..1130969a26 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -6,7 +6,7 @@ module Emails @target_url = project_merge_request_url(@project, @merge_request) mail(from: sender(@merge_request.author_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (!#{@merge_request.iid})")) + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) end def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) @@ -16,7 +16,7 @@ module Emails @target_url = project_merge_request_url(@project, @merge_request) mail(from: sender(updated_by_user_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (!#{@merge_request.iid})")) + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) end def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) @@ -26,7 +26,7 @@ module Emails @target_url = project_merge_request_url(@project, @merge_request) mail(from: sender(updated_by_user_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (!#{@merge_request.iid})")) + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) end def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) @@ -35,7 +35,7 @@ module Emails @target_url = project_merge_request_url(@project, @merge_request) mail(from: sender(updated_by_user_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (!#{@merge_request.iid})")) + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index ccbdadf010..2a877bc159 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -27,7 +27,7 @@ module Emails @target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{@note.id}") mail(from: sender(@note.author_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (!#{@merge_request.iid})")) + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) end def note_wall_email(recipient_id, note_id) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8c885b70a4..134a8c8dd4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -253,6 +253,14 @@ class MergeRequest < ActiveRecord::Base end end + def target_project_namespace + if target_project && target_project.namespace + target_project.namespace.path + else + "(removed)" + end + end + def source_branch_exists? return false unless self.source_project diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 0684461add..7dce71a677 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -86,7 +86,7 @@ class MergeRequestDiff < ActiveRecord::Base # between target and source branches def unmerged_commits commits = if merge_request.for_fork? - Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).commits_between + compare_action.commits else repository.commits_between(target_branch, source_branch) end @@ -150,7 +150,7 @@ class MergeRequestDiff < ActiveRecord::Base # between target and source branches def unmerged_diffs diffs = if merge_request.for_fork? - Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request).diffs_between_satellite + compare_action.diffs else Gitlab::Git::Diff.between(repository, source_branch, target_branch) end @@ -165,4 +165,16 @@ class MergeRequestDiff < ActiveRecord::Base def repository merge_request.target_project.repository end + + private + + def compare_action + Gitlab::Satellite::CompareAction.new( + merge_request.author, + merge_request.target_project, + merge_request.target_branch, + merge_request.source_project, + merge_request.source_branch + ) + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 6a2ca76703..39ab0b536a 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -25,6 +25,7 @@ class Milestone < ActiveRecord::Base scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } + scope :of_projects, ->(ids) { where(project_id: ids) } validates :title, presence: true validates :project, presence: true diff --git a/app/models/note.rb b/app/models/note.rb index 6f7afcd1f9..cee10ec90d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -184,9 +184,10 @@ class Note < ActiveRecord::Base return @diff_line if @diff_line if diff - Gitlab::DiffParser.new(diff).each do |full_line, type, line_code, line_new, line_old| - @diff_line = full_line if line_code == self.line_code - end + Gitlab::DiffParser.new(diff.diff.lines.to_a, diff.new_path) + .each do |full_line, type, line_code, line_new, line_old| + @diff_line = full_line if line_code == self.line_code + end end @diff_line diff --git a/app/models/project.rb b/app/models/project.rb index bd7b43723d..1da24ee649 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -207,6 +207,7 @@ class Project < ActiveRecord::Base when 'oldest' then reorder('projects.created_at ASC') when 'recently_updated' then reorder('projects.updated_at DESC') when 'last_updated' then reorder('projects.updated_at ASC') + when 'largest_repository' then reorder('projects.repository_size DESC') else reorder("namespaces.path, projects.name ASC") end end @@ -566,4 +567,8 @@ class Project < ActiveRecord::Base def forked_from?(project) forked? && project == forked_from_project end + + def update_repository_size + update_attribute(:repository_size, repository.size) + end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 163302a18f..08a5278247 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -64,7 +64,8 @@ class ProjectWiki # # Returns an initialized WikiPage instance or nil def find_page(title, version = nil) - if page = wiki.page(title, version) + page_title, page_dir = page_title_and_dir(title) + if page = wiki.page(page_title, version, page_dir) WikiPage.new(self, page, true) else nil @@ -90,6 +91,12 @@ class ProjectWiki wiki.delete_page(page, commit_details(:deleted, message, page.title)) end + def page_title_and_dir(title) + title_array = title.split("/") + title = title_array.pop + [title.gsub(/\.[^.]*$/, ""), title_array.join("/")] + end + private def create_repo! diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 76f311ed0b..b8a0a9eb58 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -175,14 +175,24 @@ class WikiPage end def save(method, *args) - if valid? && wiki.send(method, *args) - @page = wiki.wiki.paged(title) + project_wiki = wiki + if valid? && project_wiki.send(method, *args) + + page_details = if method == :update_page + @page.path + else + title + end + + page_title, page_dir = project_wiki.page_title_and_dir(page_details) + gollum_wiki = project_wiki.wiki + @page = gollum_wiki.paged(page_title, page_dir) set_attributes @persisted = true else - errors.add(:base, wiki.error_message) if wiki.error_message + errors.add(:base, project_wiki.error_message) if project_wiki.error_message @persisted = false end @persisted diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 351b446457..715b569075 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -25,6 +25,7 @@ class GitPushService project.ensure_satellite_exists project.repository.expire_cache + project.update_repository_size if push_to_existing_branch?(ref, oldrev) project.update_merge_requests(oldrev, newrev, ref, @user) diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index f28dadfb65..c58ca2c9a3 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -22,12 +22,12 @@ = f.label :color, "Background Color", class: 'control-label' .col-sm-10 = f.text_field :color, placeholder: "#AA33EE", class: "form-control" - .light Hex values as 3 double digit numbers, starting with a # sign. + .light 6 character hex values starting with a # sign. .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 = f.text_field :font, placeholder: "#224466", class: "form-control" - .light Hex values as 3 double digit numbers, starting with a # sign. + .light 6 character hex values starting with a # sign. .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 296094ab29..51ad702154 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -32,6 +32,7 @@ = visibility_level_icon(level) = label .form-actions + = hidden_field_tag :sort, params[:sort] = submit_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_projects_path, class: "btn" @@ -40,6 +41,28 @@ .title Projects (#{@projects.total_count}) .pull-right + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = @sort.humanize + - else + Name + %b.caret + %ul.dropdown-menu + %li + = link_to admin_projects_path(sort: nil) do + Name + = link_to admin_projects_path(sort: 'newest') do + Newest + = link_to admin_projects_path(sort: 'oldest') do + Oldest + = link_to admin_projects_path(sort: 'recently_updated') do + Recently updated + = link_to admin_projects_path(sort: 'last_updated') do + Last updated + = link_to admin_projects_path(sort: 'largest_repository') do + Largest repository = link_to 'New Project', new_project_path, class: "btn btn-new" %ul.well-list - @projects.each do |project| diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index d8001fd76d..fba56b5dc3 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -19,6 +19,10 @@ %li.visible-sm.visible-xs = link_to search_path, title: "Search", class: 'has_bottom_tooltip', 'data-original-title' => 'Search area' do %i.icon-search + %li + = link_to help_path, title: 'Help', class: 'has_bottom_tooltip', + 'data-original-title' => 'Help' do + %i.icon-question-sign %li = link_to public_root_path, title: "Public area", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do %i.icon-globe @@ -39,6 +43,6 @@ %li = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do %i.icon-signout - %li + %li.hidden-xs = link_to current_user, class: "profile-pic", id: 'profile-pic' do = image_tag avatar_icon(current_user.email, 26), alt: 'User activity' diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 809d46f31b..574e8bfef2 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request !#{@merge_request.iid} was closed by #{@updated_by.name}" + = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index ee434ec8cb..d6b76e906c 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.iid} was closed by #{@updated_by.name}" += "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index 0c62d439ae..6762fae7f6 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request !#{@merge_request.iid} was merged" + = "Merge Request ##{@merge_request.iid} was merged" diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 550f677fed..360da60bc3 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.iid} was merged" += "Merge Request ##{@merge_request.iid} was merged" Merge Request Url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index b8d7fbeb04..a15b8efe1f 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -17,7 +17,7 @@ Changes: - else = diff.new_path || diff.old_path \===================================== - = diff.diff + != diff.diff \ - if @compare.timeout Huge diff. To prevent performance issues it was hidden diff --git a/app/views/projects/commits/_parallel_view.html.haml b/app/views/projects/commits/_parallel_view.html.haml index 5b60ab80ba..80f5be98f2 100644 --- a/app/views/projects/commits/_parallel_view.html.haml +++ b/app/views/projects/commits/_parallel_view.html.haml @@ -2,54 +2,37 @@ - old_lines, new_lines = parallel_diff_lines(project, @commit, diff, file) - num_lines = old_lines.length -%div.text-file-parallel - %div.diff-side.diff-side-left - %table - - old_lines.each do |line| +%div.text-file + %table + - num_lines.times do |index| + - new_line = new_lines[index] + - old_line = old_lines[index] + %tr.line_holder.parallel + -# For old line + - if old_line.type == :file_created + %td.old_line= old_line.num + %td.line_content.parallel= "File was created" + - elsif old_line.type == :deleted + %td.old_line.old= old_line.num + %td.line_content{class: "parallel noteable_line old #{old_line.code}", "line_code" => old_line.code}= old_line.content + - else old_line.type == :no_change + %td.old_line= old_line.num + %td.line_content.parallel= old_line.content - %tr.line_holder.parallel - - if line.type == :file_created - %td.line_content.parallel= "File was created" - - elsif line.type == :deleted - %td.line_content{class: "parallel noteable_line old #{line.code}", "line_code" => line.code }= line.content - - else line.type == :no_change - %td.line_content.parallel= line.content + -# For new line + - if new_line.type == :file_deleted + %td.new_line= new_line.num + %td.line_content.parallel= "File was deleted" + - elsif new_line.type == :added + %td.new_line.new= new_line.num + %td.line_content{class: "parallel noteable_line new #{new_line.code}", "line_code" => new_line.code}= new_line.content + - else new_line.type == :no_change + %td.new_line= new_line.num + %td.line_content.parallel= new_line.content - %div.diff-middle - %table - - num_lines.times do |index| - %tr - - if old_lines[index].type == :deleted - %td.old_line.old= old_lines[index].num - - else - %td.old_line= old_lines[index].num + - if @reply_allowed + - comments1 = @line_notes.select { |n| n.line_code == old_line.code }.sort_by(&:created_at) + - comments2 = @line_notes.select { |n| n.line_code == new_line.code }.sort_by(&:created_at) + - unless comments1.empty? and comments2.empty? + = render "projects/notes/diff_notes_with_reply_parallel", notes1: comments1, notes2: comments2 - %td.diff_line="" - - - if new_lines[index].type == :added - %td.new_line.new= new_lines[index].num - - else - %td.new_line= new_lines[index].num - - %div.diff-side.diff-side-right - %table - - new_lines.each do |line| - - %tr.line_holder.parallel - - if line.type == :file_deleted - %td.line_content.parallel= "File was deleted" - - elsif line.type == :added - %td.line_content{class: "parallel noteable_line new #{line.code}", "line_code" => line.code }= line.content - - else line.type == :no_change - %td.line_content.parallel= line.content - -:javascript - $('.diff-side-right').on('scroll', function(){ - $('.diff-side-left, .diff-middle').scrollTop($(this).scrollTop()); - $('.diff-side-left').scrollLeft($(this).scrollLeft()); - }); - - $('.diff-side-left').on('scroll', function(){ - $('.diff-side-right, .diff-middle').scrollTop($(this).scrollTop()); // might never be relevant - $('.diff-side-right').scrollLeft($(this).scrollLeft()); - }); diff --git a/app/views/projects/edit_tree/_diff.html.haml b/app/views/projects/edit_tree/_diff.html.haml new file mode 100644 index 0000000000..cf044feb9a --- /dev/null +++ b/app/views/projects/edit_tree/_diff.html.haml @@ -0,0 +1,13 @@ +%table.text-file + - each_diff_line(diff, 1) do |line, type, line_code, line_new, line_old, raw_line| + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" + %td.old_line= "..." + %td.new_line= "..." + %td.line_content.matched= line + - else + %td.old_line + = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code + %td.new_line= link_to raw(type == "old" ? " " : line_new) , "##{line_code}", id: line_code + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line) + diff --git a/app/views/projects/edit_tree/preview.html.haml b/app/views/projects/edit_tree/preview.html.haml new file mode 100644 index 0000000000..fc6d3bfbc2 --- /dev/null +++ b/app/views/projects/edit_tree/preview.html.haml @@ -0,0 +1,26 @@ +.diff-file + .diff-content + - if gitlab_markdown?(@blob.name) + .file-content.wiki + = preserve do + = markdown(@content) + - elsif markup?(@blob.name) + .file-content.wiki + = raw GitHub::Markup.render(@blob.name, @content) + - else + .file-content.code + - unless @diff.empty? + %table.text-file + - @diff.each do |line, type, line_code, line_new, line_old, raw_line| + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" + %td.old_line= "..." + %td.new_line= "..." + %td.line_content.matched= line + - else + %td.old_line + = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code + %td.new_line= link_to raw(type == "old" ? " " : line_new) , "##{line_code}", id: line_code + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line) + - else + %p.nothing_here_message No changes. diff --git a/app/views/projects/edit_tree/show.html.haml b/app/views/projects/edit_tree/show.html.haml index 3f2e98f3a7..48babb43aa 100644 --- a/app/views/projects/edit_tree/show.html.haml +++ b/app/views/projects/edit_tree/show.html.haml @@ -1,8 +1,11 @@ %h3.page-title Edit mode .file-editor = form_tag(project_edit_tree_path(@project, @id), method: :put, class: "form-horizontal") do - .file-holder + .file-holder.file .file-title + .btn-group.js-edit-mode.left-options + = link_to 'Edit', '#editor', class: 'active hover btn btn-tiny' + = link_to editing_preview_title(@blob.name), '#preview', class: 'btn btn-tiny', 'data-preview-url' => preview_project_edit_tree_path(@project, @id) %i.icon-file %span.file_name = @path @@ -13,7 +16,8 @@ .btn-group.tree-btn-group = link_to "Cancel", @after_edit_path, class: "btn btn-tiny btn-cancel", data: { confirm: leave_edit_message } .file-content.code - %pre#editor= @blob.data + %pre.js-edit-mode-pane#editor= @blob.data + .js-edit-mode-pane#preview.hide .form-group.commit_message-group = label_tag 'commit_message', class: "control-label" do @@ -45,3 +49,28 @@ $("#file-content").val(editor.getValue()); $(".file-editor form").submit(); }); + + var editModePanes = $('.js-edit-mode-pane'), + editModeLinks = $('.js-edit-mode a'); + + editModeLinks.click(function(event) { + event.preventDefault(); + + var currentLink = $(this), + paneId = currentLink.attr('href'), + currentPane = editModePanes.filter(paneId); + + editModeLinks.removeClass('active hover'); + currentLink.addClass('active hover'); + editModePanes.hide(); + + if (paneId == '#preview') { + $.post(currentLink.data('preview-url'), { content: editor.getValue() }, function(response) { + currentPane.empty().append(response); + currentPane.fadeIn(200); + }) + } else { + currentPane.fadeIn(200); + editor.focus() + } + }) diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index aae101cf40..425dcb45dd 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -1,22 +1,24 @@ = form_for [@project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| - %strong.append-right-10 - Assignee: + .row + .col-md-6 + %strong.append-right-10 + Assignee: - - if can?(current_user, :modify_issue, @issue) - = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control', selected: @issue.assignee_id) - - elsif issue.assignee - = link_to_member(@project, @issue.assignee) - - else - None + - if can?(current_user, :modify_issue, @issue) + = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control', selected: @issue.assignee_id) + - elsif issue.assignee + = link_to_member(@project, @issue.assignee) + - else + None - .pull-right - %strong.append-right-10 - Milestone: - - if can?(current_user, :modify_issue, @issue) - = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone (none):" }, {class: 'select2 select2-compact'}) - = hidden_field_tag :issue_context - = f.submit class: 'btn' - - elsif issue.milestone - = link_to issue.milestone.title, project_milestone_path - - else - None + .col-md-6.text-right + %strong.append-right-10 + Milestone: + - if can?(current_user, :modify_issue, @issue) + = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact'}) + = hidden_field_tag :issue_context + = f.submit class: 'btn' + - elsif issue.milestone + = link_to issue.milestone.title, project_milestone_path + - else + None diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 124eb53571..b6d3a8edf4 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,7 +1,7 @@ %h3.page-title Issue ##{@issue.iid} - %span.pull-right + %span.pull-right.issue-btn-group - if can?(current_user, :write_issue, @project) = link_to new_project_issue_path(@project), class: "btn btn-grouped", title: "New Issue", id: "new_issue_link" do %i.icon-plus @@ -16,28 +16,29 @@ %i.icon-edit Edit -.votes-holder - #votes= render 'votes/votes_block', votable: @issue +.clearfix + .votes-holder + #votes= render 'votes/votes_block', votable: @issue -.back-link - = link_to project_issues_path(@project) do - ← To issues list - %span.milestone-nav-link - - if @issue.milestone - | - %span.light Milestone - = link_to project_milestone_path(@project, @issue.milestone) do - = @issue.milestone.title + .back-link + = link_to project_issues_path(@project) do + ← To issues list + %span.milestone-nav-link + - if @issue.milestone + | + %span.light Milestone + = link_to project_milestone_path(@project, @issue.milestone) do + = @issue.milestone.title .issue-box{ class: issue_box_class(@issue) } - .state - %span.state-label + .state.clearfix + .state-label.col-sm-2.col-xs-12 - if @issue.closed? Closed - else Open - %span.creator + %span.creator.col-sm-9.col-xs-12 Created by #{link_to_member(@project, @issue.author)} #{time_ago_with_tooltip(@issue.created_at)} %h4.title diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 0fe2d1d980..ddff3dbead 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -14,33 +14,6 @@ - @merge_request.errors.full_messages.each do |msg| %div= msg - .merge-request-branches - .form-group - = label_tag nil, class: 'control-label' do - From - .col-sm-10 - .clearfix - .pull-left - = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted? }) - .pull-left -   - = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2'}) - .mr_source_commit - %br - .form-group - = label_tag nil, class: 'control-label' do - To - .col-sm-10 - .clearfix - .pull-left - - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] - = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted? }) - .pull-left -   - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2'}) - .mr_target_commit - - %hr .merge-request-form-info .form-group = f.label :title, class: 'control-label' do @@ -51,6 +24,23 @@ .col-sm-10 = f.text_area :description, class: "form-control js-gfm-input", rows: 14 %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + %hr + .form-group + .issue-assignee + = f.label :assignee_id, class: 'control-label' do + %i.icon-user + Assign to + .col-sm-10 + = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id) +   + = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' + .form-group + .issue-milestone + = f.label :milestone_id, class: 'control-label' do + %i.icon-time + Milestone + .col-sm-10= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'}) + .form-actions - if @merge_request.new_record? @@ -66,20 +56,7 @@ :javascript disableButtonIfEmptyField("#merge_request_title", ".btn-save"); - - var source_branch = $("#merge_request_source_branch") - , target_branch = $("#merge_request_target_branch") - , target_project = $("#merge_request_target_project_id"); - - $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: source_branch.val() }); - $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); - - target_project.on("change", function() { - $.get("#{update_branches_project_merge_requests_path(@source_project)}", {target_project_id: $(this).val() }); - }); - source_branch.on("change", function() { - $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: $(this).val() }); - }); - target_branch.on("change", function() { - $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); + $('.assign-to-me-link').on('click', function(e){ + $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); + e.preventDefault(); }); diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 980ac12674..d1cab89a35 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -11,13 +11,9 @@ - if merge_request.for_fork? %span.light #{merge_request.source_project_namespace}: - = merge_request.source_branch - %i.icon-angle-right.light - = merge_request.target_branch - - else - = merge_request.source_branch - %i.icon-angle-right.light - = merge_request.target_branch + = truncate merge_request.source_branch, length: 25 + %i.icon-angle-right.light + = merge_request.target_branch .merge-request-info - if merge_request.author authored by #{link_to_member(merge_request.source_project, merge_request.author)} diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml new file mode 100644 index 0000000000..a8b774a3cd --- /dev/null +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -0,0 +1,84 @@ +%h3.page-title Compare branches for new Merge Request +%hr + += form_for [@project, @merge_request], url: new_project_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline" } do |f| + .hide.alert.alert-danger.mr-compare-errors + .merge-request-branches.row + .col-md-6 + .panel.panel-default + .panel-heading + %strong Source branch + .panel-body + = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted? }) +   + = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2'}) + .panel-footer + .mr_source_commit + + .col-md-6 + .panel.panel-default + .panel-heading + %strong Target branch + .panel-body + - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] + = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted? }) +   + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2'}) + .panel-footer + .mr_target_commit + + -if @merge_request.errors.any? + .alert.alert-danger + - @merge_request.errors.full_messages.each do |msg| + %div= msg + + - if @merge_request.source_branch.present? && @merge_request.target_branch.present? + .light-well + %center + %h4 + There isn't anything to merge. + %p.slead + - if @merge_request.source_branch == @merge_request.target_branch + You'll need to use different branch names to get a valid comparison. + - else + %span.label-branch #{@merge_request.source_branch} + and + %span.label-branch #{@merge_request.target_branch} + are the same. + + + %hr + = f.submit 'Compare branches', class: "btn btn-primary mr-compare-btn" + +:javascript + var source_branch = $("#merge_request_source_branch") + , target_branch = $("#merge_request_target_branch") + , target_project = $("#merge_request_target_project_id"); + + $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: source_branch.val() }); + $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); + + target_project.on("change", function() { + $.get("#{update_branches_project_merge_requests_path(@source_project)}", {target_project_id: $(this).val() }); + }); + source_branch.on("change", function() { + $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: $(this).val() }); + $(".mr-compare-errors").fadeOut(); + $(".mr-compare-btn").enable(); + }); + target_branch.on("change", function() { + $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); + $(".mr-compare-errors").fadeOut(); + $(".mr-compare-btn").enable(); + }); + + +:coffeescript + + $(".merge-request-form").on 'submit', -> + if $("#merge_request_source_branch").val() is "" or $('#merge_request_target_branch').val() is "" + $(".mr-compare-errors").html("You must select source and target branch to proceed") + $(".mr-compare-errors").fadeIn() + event.preventDefault() + return + diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml new file mode 100644 index 0000000000..b5479be708 --- /dev/null +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -0,0 +1,82 @@ +%h3.page-title + New merge request +%p.slead + From + %strong.monospace + #{@merge_request.source_project_namespace}:#{@merge_request.source_branch} + into + %strong.monospace + #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} + + %span.pull-right + = link_to 'Change branches', new_project_merge_request_path(@project) + += form_for [@project, @merge_request], html: { class: "merge-request-form" } do |f| + .panel.panel-default + + .panel-body + .form-group + .light + = f.label :title do + = "Title *" + = f.text_field :title, class: "form-control input-lg js-gfm-input", maxlength: 255, rows: 5, required: true + .form-group + .light + = f.label :description, "Description" + = f.text_area :description, class: "form-control js-gfm-input", rows: 10 + %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + .form-group + .issue-assignee + = f.label :assignee_id do + %i.icon-user + Assign to + %div + = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id) +   + = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' + .form-group + .issue-milestone + = f.label :milestone_id do + %i.icon-time + Milestone + %div= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'}) + .panel-footer + - if @target_repo.contribution_guide + - contribution_guide_url = project_blob_path(@target_project, tree_join(@target_repo.root_ref, @target_repo.contribution_guide.name)) + %p + Please review the + %strong #{link_to "guidelines for contribution", contribution_guide_url} + to this repository. + = f.hidden_field :source_project_id + = f.hidden_field :target_project_id + = f.hidden_field :target_branch + = f.hidden_field :source_branch + = f.submit 'Submit merge request', class: "btn btn-create" + +.mr-compare + %div.ui-box + .title + Commits (#{@commits.count}) + - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + %ul.well-list + - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE)).each do |commit| + = render "projects/commits/inline_commit", commit: commit, project: @project + %li.warning-row.unstyled + other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. + - else + %ul.well-list= render Commit.decorate(@commits), project: @project + + %h4 Changes + - if @diffs.present? + = render "projects/commits/diffs", diffs: @diffs, project: @project + - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + .bs-callout.bs-callout-danger + %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. + %p To preserve performance the line changes are not shown. + + +:javascript + $('.assign-to-me-link').on('click', function(e){ + $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); + e.preventDefault(); + }); diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml index 693c2057a0..8372afa61b 100644 --- a/app/views/projects/merge_requests/branch_from.js.haml +++ b/app/views/projects/merge_requests/branch_from.js.haml @@ -1,7 +1,2 @@ :plain $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}"); - var mrTitle = $('#merge_request_title'); - - if(mrTitle.val().length == 0) { - mrTitle.val("#{params[:ref].titleize.humanize}"); - } diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index 8ee0e1a8d4..c24e591672 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,3 +1,4 @@ -%h3.page-title New Merge Request -%hr -= render 'form' +- if @commits.present? + = render 'new_submit' +- else + = render 'new_compare' diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 2bd850426a..5c6734fd24 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -1,22 +1,24 @@ = form_for [@project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| - %strong.append-right-10 - Assignee: + .row + .col-md-6 + %strong.append-right-10 + Assignee: - - if can?(current_user, :modify_merge_request, @merge_request) - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control', selected: @merge_request.assignee_id) - - elsif merge_request.assignee - = link_to_member(@project, @merge_request.assignee) - - else - None + - if can?(current_user, :modify_merge_request, @merge_request) + = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control', selected: @merge_request.assignee_id) + - elsif merge_request.assignee + = link_to_member(@project, @merge_request.assignee) + - else + None - .pull-right - %strong.append-right-10 - Milestone: - - if can?(current_user, :modify_merge_request, @merge_request) - = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone (none):" }, {class: 'select2 select2-compact'}) - = hidden_field_tag :merge_request_context - = f.submit class: 'btn' - - elsif merge_request.milestone - = link_to merge_request.milestone.title, project_milestone_path - - else - None + .col-md-6.text-right + %strong.append-right-10 + Milestone: + - if can?(current_user, :modify_merge_request, @merge_request) + = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact'}) + = hidden_field_tag :merge_request_context + = f.submit class: 'btn' + - elsif merge_request.milestone + = link_to merge_request.milestone.title, project_milestone_path + - else + None diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 8855982a2e..435e916c6d 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,6 +1,6 @@ .issue-box{ class: issue_box_class(@merge_request) } - .state - %span.state-label + .state.clearfix + %span.state-label.col-sm-2.col-xs-12 - if @merge_request.merged? Merged - elsif @merge_request.closed? @@ -8,7 +8,7 @@ - else Open - %span.creator + %span.creator.col-sm-9.col-xs-12 Created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} %h4.title 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 c175d2f6b4..507a9e507f 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -26,4 +26,4 @@ .ci_widget.ci-error{style: "display:none"} %i.icon-remove - %strong Cannot connect to CI server. Please check your setting + %strong Cannot connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 7676fc137c..2c905413bc 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,7 +1,7 @@ %h3.page-title = "Merge Request ##{@merge_request.iid}" - %span.pull-right + %span.pull-right.issue-btn-group - if can?(current_user, :modify_merge_request, @merge_request) - if @merge_request.open? .btn-group.pull-left @@ -39,4 +39,4 @@ - else %span= @merge_request.source_branch → - %spanh= @merge_request.target_branch + %span= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index c9ecbceaf5..80fe540489 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -21,14 +21,6 @@ #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} = render "projects/merge_requests/show/remove_source_branch" - - if !@closes_issues.empty? && @merge_request.open? - .alert.alert-info.alert-info - %span - %i.icon-ok - Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} - = succeed '.' do - != gfm(@closes_issues.map { |i| "##{i.iid}" }.to_sentence) - - unless @commits.any? %h4 Nothing to merge %p @@ -38,3 +30,12 @@ %span.label-branch #{@merge_request.target_branch} %br Try to use different branches or push new code. + + - if !@closes_issues.empty? && @merge_request.open? + .panel-footer + %span + %i.icon-ok + Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} + = succeed '.' do + != gfm(@closes_issues.map { |i| "##{i.iid}" }.to_sentence) + diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 06cf994678..5c5df46d33 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -100,7 +100,7 @@ %ul.bordered-list - @users.each do |user| %li - = link_to user, title: user.name, class: "dark" do + = 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 diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index 2012aa021b..399ce30d1a 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -1,34 +1,33 @@ - note1 = notes1.first # example note - note2 = notes2.first # example note +-# Check if line want not changed since comment was left +/- if !defined?(line) || line == note.diff_line %tr.notes_holder.js-toggle-content - -# Check if line want not changed since comment was left - /- if !defined?(line1) || line1 == note1.diff_line - if note1 + %td.notes_line + %span.btn.disabled + %i.icon-comment + = notes1.count %td.notes_content %ul.notes{ rel: note1.discussion_id } = render notes1 + = render "projects/notes/discussion_reply_button", note: note1 - %td.notes_line2 - %span.btn.disabled.parallel-comment - %i.icon-comment - = notes1.count - else %td= "" %td= "" - %td= "" - - -# Check if line want not changed since comment was left - /- if !defined?(line2) || line2 == note2.diff_line - if note2 %td.notes_line - %span.btn.disabled.parallel-comment + %span.btn.disabled %i.icon-comment = notes2.count %td.notes_content %ul.notes{ rel: note2.discussion_id } = render notes2 + = render "projects/notes/discussion_reply_button", note: note2 - else %td= "" %td= "" + diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 3bd592e398..052661962e 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -7,4 +7,4 @@ = render "projects/notes/form" :javascript - new Notes("#{project_notes_path(target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}) + new Notes("#{project_notes_path(target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}) diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 8cb7fa8aa0..1ce292a02d 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -9,6 +9,6 @@ %span Page slug = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project) %p.hint - Please don't use spaces and slashes + Please don't use spaces. .modal-footer = link_to 'Build', '#', class: 'build-new-wiki btn btn-create' diff --git a/bin/pkgr_before_precompile.sh b/bin/pkgr_before_precompile.sh new file mode 100755 index 0000000000..283abb6a0c --- /dev/null +++ b/bin/pkgr_before_precompile.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +for file in config/*.yml.example; do + cp ${file} config/$(basename ${file} .example) +done + +# Allow to override the Gitlab URL from an environment variable, as this will avoid having to change the configuration file for simple deployments. +config=$(echo '<% gitlab_url = URI(ENV["GITLAB_URL"] || "http://localhost:80") %>' | cat - config/gitlab.yml) +echo "$config" > config/gitlab.yml +sed -i "s/host: localhost/host: <%= gitlab_url.host %>/" config/gitlab.yml +sed -i "s/port: 80/port: <%= gitlab_url.port %>/" config/gitlab.yml +sed -i "s/https: false/https: <%= gitlab_url.scheme == 'https' %>/" config/gitlab.yml + +# No need for config file. Will be taken care of by REDIS_URL env variable +rm config/resque.yml + +# Set default unicorn.rb file +echo "" > config/unicorn.rb + +# Required for assets precompilation +sudo service postgresql start diff --git a/config/environments/production.rb b/config/environments/production.rb index ad3c03d8fc..47f7e17aeb 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -53,7 +53,7 @@ Gitlab::Application.configure do else "redis://localhost:6379" end - config.cache_store = :redis_store, resque_url + config.cache_store = :redis_store, resque_url, {namespace: 'cache:gitlab'} # Enable serving of images, stylesheets, and JavaScripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d07f5d1fef..e4a67be428 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -19,6 +19,11 @@ production: &base port: 80 https: false + # Uncommment this line below if your ssh host is different from HTTP/HTTPS one + # (you'd obviously need to replace ssh.host_example.com with your own host). + # Otherwise, ssh host will be set to the `host:` value above + # ssh_host: ssh.host_example.com + # Uncomment and customize the last line to run in a non-root path # WARNING: We recommend creating a FQDN to host GitLab in a root path instead of this. # Note that four settings need to be changed for this to work. @@ -102,7 +107,7 @@ production: &base # ## :id - Issue id (from commit messages) # issues_url: "http://redmine.sample/issues/:id" # - # ## If not nil, linkis to creating new issues will be replaced with this + # ## If not nil, links to creating new issues will be replaced with this # ## Use placeholders: # ## :project_id - GitLab project identifier # ## :issues_tracker_id - Project Name or Id in external issue tracker @@ -117,6 +122,7 @@ production: &base ## Gravatar gravatar: enabled: true # Use user avatar image from Gravatar.com (default: true) + # gravatar urls: possible placeholders: %{hash} %{size} %{email} # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=mm # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=mm diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4e8b3f304d..97f2954640 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -73,6 +73,7 @@ Settings.gitlab['default_projects_limit'] ||= 10 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? Settings.gitlab['default_theme'] = Gitlab::Theme::MARS if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= 'localhost' +Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' @@ -117,7 +118,7 @@ Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitla Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? Settings.gitlab_shell['repos_path'] ||= Settings.gitlab['user_home'] + '/repositories/' -Settings.gitlab_shell['ssh_host'] ||= (Settings.gitlab.host || 'localhost') +Settings.gitlab_shell['ssh_host'] ||= Settings.gitlab.ssh_host Settings.gitlab_shell['ssh_port'] ||= 22 Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 6875fa74ed..d0065b63e5 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -18,4 +18,16 @@ if File.exists?(aws_file) config.fog_authenticated_url_expiration = 1 << 29 # optional time (in seconds) that authenticated urls will be valid. # when fog_public is false and provider is AWS or Google, defaults to 600 end + + # Mocking Fog requests, based on: https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Test-Fog-based-uploaders + if Rails.env.test? + Fog.mock! + connection = ::Fog::Storage.new( + :aws_access_key_id => AWS_CONFIG['access_key_id'], + :aws_secret_access_key => AWS_CONFIG['secret_access_key'], + :provider => 'AWS', + :region => AWS_CONFIG['region'] + ) + connection.directories.create(:key => AWS_CONFIG['bucket']) + end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 50669ece7a..d5cb110e88 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -223,6 +223,7 @@ Devise.setup do |config| method: Gitlab.config.ldap['method'], bind_dn: Gitlab.config.ldap['bind_dn'], password: Gitlab.config.ldap['password'], + filter: Gitlab.config.ldap['user_filter'], name_proc: email_stripping_proc end @@ -244,4 +245,4 @@ Devise.setup do |config| config.omniauth provider['name'].to_sym, *provider_arguments end -end +end \ No newline at end of file diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index f80b67a554..5fe5270236 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -2,7 +2,7 @@ Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. - servers: Gitlab::Application.config.cache_store.last, # re-use the Redis config from the Rails cache store + servers: Gitlab::Application.config.cache_store[1], # re-use the Redis config from the Rails cache store key: '_gitlab_session', secure: Gitlab.config.gitlab.https, httponly: true, diff --git a/config/routes.rb b/config/routes.rb index 52e19e6e43..045042349f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -201,7 +201,9 @@ Gitlab::Application.routes.draw do resources :blob, only: [:show, :destroy], constraints: {id: /.+/} resources :raw, only: [:show], constraints: {id: /.+/} resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ } - resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit' + resources :edit_tree, only: [:show, :update], constraints: { id: /.+/ }, path: 'edit' do + post :preview, on: :member + end resources :new_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'new' resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} @@ -218,7 +220,7 @@ Gitlab::Application.routes.draw do end end - resources :wikis, only: [:show, :edit, :destroy, :create], constraints: {id: /[a-zA-Z.0-9_\-]+/} do + resources :wikis, only: [:show, :edit, :destroy, :create], constraints: {id: /[a-zA-Z.0-9_\-\/]+/} do collection do get :pages put ':id' => 'wikis#update' diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 9303ab9330..164bb63780 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -40,7 +40,8 @@ Gitlab::Seeder.quiet do import_url: url, namespace_id: group.id, name: project_path.titleize, - description: Faker::Lorem.sentence + description: Faker::Lorem.sentence, + visibility_level: Gitlab::VisibilityLevel.values.sample } project = Projects::CreateService.new(User.first, params).execute diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index cb08a7c253..62fd0d84ea 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -1,45 +1,33 @@ Gitlab::Seeder.quiet do - (1..100).each do |i| - # Random Project - project = Project.all.sample + Project.all.reject(&:empty_repo?).each do |project| + branches = project.repository.branch_names - # Random user - user = project.team.users.sample + branches.each do |branch_name| + break if branches.size < 2 + source_branch = branches.pop + target_branch = branches.pop - next unless user + # Random user + user = project.team.users.sample + next unless user - next if project.empty_repo? + params = { + source_branch: source_branch, + target_branch: target_branch, + title: Faker::Lorem.sentence(6), + description: Faker::Lorem.sentences(3).join(" ") + } - branches = project.repository.branch_names.sample(2) + merge_request = MergeRequests::CreateService.new(project, user, params).execute - next if branches.uniq.size < 2 - - user_id = user.id - - Gitlab::Seeder.by_user(user) do - MergeRequest.seed(:id, [{ - id: i, - source_branch: branches.first, - target_branch: branches.last, - source_project_id: project.id, - target_project_id: project.id, - author_id: user_id, - assignee_id: user_id, - milestone: project.milestones.sample, - title: Faker::Lorem.sentence(6) - }]) + if merge_request.valid? + merge_request.assignee = user + merge_request.milestone = project.milestones.sample + merge_request.save + print '.' + else + print 'F' + end end - print('.') end end - -MergeRequest.all.map do |mr| - mr.set_iid - mr.save -end - -puts 'Load diffs for Merge Requests (it will take some time)...' -MergeRequest.all.each do |mr| - mr.reload_code - print '.' -end diff --git a/db/migrate/20140415124820_limits_to_mysql.rb b/db/migrate/20140415124820_limits_to_mysql.rb new file mode 100644 index 0000000000..3f6e62617c --- /dev/null +++ b/db/migrate/20140415124820_limits_to_mysql.rb @@ -0,0 +1 @@ +require_relative 'limits_to_mysql' diff --git a/db/migrate/20140416074002_add_index_on_iid.rb b/db/migrate/20140416074002_add_index_on_iid.rb new file mode 100644 index 0000000000..85269e2a03 --- /dev/null +++ b/db/migrate/20140416074002_add_index_on_iid.rb @@ -0,0 +1,32 @@ +class AddIndexOnIid < ActiveRecord::Migration + def change + RemoveDuplicateIid.clean(Issue) + RemoveDuplicateIid.clean(MergeRequest, 'target_project_id') + RemoveDuplicateIid.clean(Milestone) + + add_index :issues, [:project_id, :iid], unique: true + add_index :merge_requests, [:target_project_id, :iid], unique: true + add_index :milestones, [:project_id, :iid], unique: true + end +end + +class RemoveDuplicateIid + def self.clean(klass, project_field = 'project_id') + duplicates = klass.find_by_sql("SELECT iid, #{project_field} FROM #{klass.table_name} GROUP BY #{project_field}, iid HAVING COUNT(*) > 1") + + duplicates.each do |duplicate| + project_id = duplicate.send(project_field) + iid = duplicate.iid + items = klass.of_projects(project_id).where(iid: iid) + + if items.size > 1 + puts "Remove #{klass.name} duplicates for iid: #{iid} and project_id: #{project_id}" + items.shift + items.each do |item| + item.destroy + puts '.' + end + end + end + end +end diff --git a/db/migrate/20140416185734_index_on_current_sign_in_at.rb b/db/migrate/20140416185734_index_on_current_sign_in_at.rb new file mode 100644 index 0000000000..0bf80ce154 --- /dev/null +++ b/db/migrate/20140416185734_index_on_current_sign_in_at.rb @@ -0,0 +1,5 @@ +class IndexOnCurrentSignInAt < ActiveRecord::Migration + def change + add_index :users, :current_sign_in_at + end +end diff --git a/db/migrate/20140428105831_add_notes_index_updated_at.rb b/db/migrate/20140428105831_add_notes_index_updated_at.rb new file mode 100644 index 0000000000..6c25570f12 --- /dev/null +++ b/db/migrate/20140428105831_add_notes_index_updated_at.rb @@ -0,0 +1,5 @@ +class AddNotesIndexUpdatedAt < ActiveRecord::Migration + def change + add_index :notes, :updated_at + end +end diff --git a/db/migrate/20140502115131_add_repo_size_to_db.rb b/db/migrate/20140502115131_add_repo_size_to_db.rb new file mode 100644 index 0000000000..7361d1a944 --- /dev/null +++ b/db/migrate/20140502115131_add_repo_size_to_db.rb @@ -0,0 +1,5 @@ +class AddRepoSizeToDb < ActiveRecord::Migration + def change + add_column :projects, :repository_size, :float, default: 0 + end +end diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb new file mode 100644 index 0000000000..eed6d36681 --- /dev/null +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -0,0 +1,21 @@ +class MigrateRepoSize < ActiveRecord::Migration + def up + Project.reset_column_information + Project.find_each(batch_size: 500) do |project| + begin + if project.empty_repo? + print '-' + else + project.update_repository_size + print '.' + end + rescue + print 'F' + end + end + puts 'Done' + end + + def down + end +end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb new file mode 100644 index 0000000000..2b7afae6d7 --- /dev/null +++ b/db/migrate/limits_to_mysql.rb @@ -0,0 +1,10 @@ +class LimitsToMysql < ActiveRecord::Migration + def up + return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ + + change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647 + change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 + change_column :snippets, :content, :text, limit: 2147483647 + change_column :notes, :st_diff, :text, limit: 2147483647 + end +end diff --git a/db/schema.rb b/db/schema.rb index b4b084e60f..a6f883914e 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: 20140414131055) do +ActiveRecord::Schema.define(version: 20140502125220) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -112,6 +112,7 @@ ActiveRecord::Schema.define(version: 20140414131055) do add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree + add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["title"], name: "index_issues_on_title", using: :btree @@ -162,6 +163,7 @@ ActiveRecord::Schema.define(version: 20140414131055) do add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree + add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree create_table "milestones", force: true do |t| @@ -176,6 +178,7 @@ ActiveRecord::Schema.define(version: 20140414131055) do end add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree + add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree create_table "namespaces", force: true do |t| @@ -218,6 +221,7 @@ ActiveRecord::Schema.define(version: 20140414131055) do add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree + add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree create_table "project_group_links", force: true do |t| t.integer "project_id", null: false @@ -247,6 +251,7 @@ ActiveRecord::Schema.define(version: 20140414131055) do t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false t.string "import_status" + t.float "repository_size", default: 0.0 end add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree @@ -348,7 +353,6 @@ ActiveRecord::Schema.define(version: 20140414131055) do t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" - t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" @@ -356,11 +360,13 @@ ActiveRecord::Schema.define(version: 20140414131055) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false + t.datetime "last_credential_check_at" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree + add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree diff --git a/doc/api/README.md b/doc/api/README.md index b1740f3579..4c40589fd4 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -13,7 +13,7 @@ + [Merge Requests](merge_requests.md) + [Issues](issues.md) + [Milestones](milestones.md) -+ [Notes](notes.md) ++ [Notes](notes.md) (comments) + [Deploy Keys](deploy_keys.md) + [System Hooks](system_hooks.md) + [Groups](groups.md) @@ -21,9 +21,11 @@ ## Clients + [php-gitlab-api](https://github.com/m4tthumphrey/php-gitlab-api) - PHP ++ [Laravel API Wrapper for GitLab CE](https://github.com/adamgoose/gitlab) - PHP / [Laravel](http://laravel.com) + [Ruby Wrapper](https://github.com/NARKOZ/gitlab) - Ruby + [python-gitlab](https://github.com/Itxaka/python-gitlab) - Python + [java-gitlab-api](https://github.com/timols/java-gitlab-api) - Java ++ [node-gitlab](https://github.com/moul/node-gitlab) - Node.js ## Introduction diff --git a/doc/api/issues.md b/doc/api/issues.md index 823b72f5b0..d18506f9ce 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -193,3 +193,7 @@ Parameters: + `id` (required) - The project ID + `issue_id` (required) - The ID of the issue + +## Comments on issues + +Comments are done via the notes resource. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 4e864ae107..d5b106729c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -105,10 +105,11 @@ POST /projects/:id/merge_requests Parameters: + `id` (required) - The ID of a project -+ `source_branch` (required) - The source branch -+ `target_branch` (required) - The target branch -+ `assignee_id` (optional) - Assignee user ID -+ `title` (required) - Title of MR ++ `source_branch` (required) - The source branch ++ `target_branch` (required) - The target branch ++ `assignee_id` (optional) - Assignee user ID ++ `title` (required) - Title of MR ++ `target_project_id` (optional) - The target project (numeric id) ```json { @@ -257,3 +258,7 @@ Parameters: } ] ``` + +## Comments on issues + +Comments are done via the notes resource. diff --git a/doc/api/notes.md b/doc/api/notes.md index b15ebdd2ba..e9ad6e00c7 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -1,3 +1,5 @@ +Notes can be wall notes or comments on snippets, issues or merge requests. + ## Wall ### List project wall notes diff --git a/doc/api/users.md b/doc/api/users.md index 2d5dedb3a3..2b927c3077 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -220,6 +220,18 @@ Parameters: + **none** +## List SSH keys for user + +Get a list of a specified user's SSH keys. Available only for admin + +``` +GET /users/:uid/keys +``` + +Parameters: + ++ `uid` (required) - id of specified user + ## Single SSH key @@ -286,3 +298,18 @@ Parameters: + `id` (required) - SSH key ID +## Delete SSH key + +Deletes key owned by a specified user. Available only for admin. + +``` +DELETE /users/:uid/keys/:id +``` + +Parameters: + ++ `uid` (required) - id of specified user ++ `id` (required) - SSH key ID + +Will return `200 Ok` on success, or `404 Not found` if either user or key cannot be found. + diff --git a/doc/development/README.md b/doc/development/README.md index aa59eb2c3e..eb88b6c860 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -1,2 +1,5 @@ -+ [Architecture](architecture.md) -+ [Shell commands](shell_commands.md) +## Development + ++ [Architecture](architecture.md) of GitLab ++ [Shell commands](shell_commands.md) in the GitLab codebase ++ [Rake tasks](rake_tasks.md) for development diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md new file mode 100644 index 0000000000..9e75b3a627 --- /dev/null +++ b/doc/development/rake_tasks.md @@ -0,0 +1,25 @@ +# Rake tasks for developers + +## Setup db with developer seeds: + +Note that if your db user does not have advanced privilegies you must create db manually before run this command + +``` +bundle exec rake setup +``` + +## Run tests + +This runs all test suite present in GitLab + +``` +bundle exec rake test +``` + +## Generate searchable docs for source code + +You can find results under `doc/code` directory + +``` +bundle exec rake gitlab:generate_docs +``` diff --git a/doc/install/installation.md b/doc/install/installation.md index 8604270213..7385324505 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,10 +1,10 @@ # Select Version to Install -Make sure you view this installation guide from the branch (version) of GitLab you would like to install. In most cases +Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install. In most cases this should be the highest numbered stable branch (example shown below). ![capture](http://i.imgur.com/d2AlIVj.png) -If this is unclear check the [GitLab Blog](https://www.gitlab.com/blog/) for installation guide links by version. +If the highest number stable branch is unclear please check the [GitLab Blog](https://www.gitlab.com/blog/) for installation guide links by version. # Important notes @@ -27,10 +27,9 @@ The GitLab installation consists of setting up the following components: 1. Packages / Dependencies 2. Ruby 3. System Users -4. GitLab shell -5. Database -6. GitLab -7. Nginx +4. Database +5. GitLab +6. Nginx # 1. Packages / Dependencies @@ -119,32 +118,10 @@ Create a `git` user for Gitlab: sudo adduser --disabled-login --gecos 'GitLab' git - -# 4. GitLab shell - -GitLab Shell is an ssh access and repository management software developed specially for GitLab. - - # Go to home directory - cd /home/git - - # Clone gitlab shell - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-shell.git -b v1.9.3 - - cd gitlab-shell - - sudo -u git -H cp config.yml.example config.yml - - # Edit config and replace gitlab_url - # with something like 'http://domain.com/' - sudo -u git -H editor config.yml - - # Do setup - sudo -u git -H ./bin/install - - -# 5. Database +# 4. Database We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](database_mysql.md). +NOTE: because we need to make use of extensions you need at least pgsql 9.1. # Install the database packages sudo apt-get install -y postgresql-9.1 postgresql-client libpq-dev @@ -153,7 +130,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da sudo -u postgres psql -d template1 # Create a user for GitLab. - template1=# CREATE USER git; + template1=# CREATE USER git CREATEDB; # Create the GitLab production database & grant all privileges on database template1=# CREATE DATABASE gitlabhq_production OWNER git; @@ -165,7 +142,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da sudo -u git -H psql -d gitlabhq_production -# 6. GitLab +# 5. GitLab # We'll install GitLab into home directory of the user "git" cd /home/git @@ -276,6 +253,18 @@ that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. # When done you see 'Administrator account created:' +## Install GitLab shell + +GitLab Shell is an ssh access and repository management software developed specially for GitLab. + + # Go to the Gitlab installation folder: + cd /home/git/gitlab + + # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): + sudo -u git -H bundle exec rake gitlab:shell:install[v1.9.3] REDIS_URL=redis://localhost:6379 RAILS_ENV=production + + # By default, the gitlab-shell config is generated from your main gitlab config. You can review (and modify) it as follows: + sudo -u git -H editor /home/git/gitlab-shell/config.yml ## Install Init Script @@ -313,7 +302,8 @@ Check if GitLab and its environment are configured correctly: # or sudo /etc/init.d/gitlab restart -# 7. Nginx + +# 6. Nginx **Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 62e21dc72b..0fe015b2d3 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -74,6 +74,9 @@ Apart from a local hard drive you can also mount a volume that supports the netw If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab. +## Database + +If you want to run the database separately, the **recommended** database size is **1 MB per user** # Supported webbrowsers diff --git a/doc/integration/README.md b/doc/integration/README.md index 3e8d329d55..8318113ce9 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -6,3 +6,4 @@ See the documentation below for details on how to configure these services. + [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. + [LDAP](ldap.md) Set up sign in via LDAP + [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, and Google via OAuth. ++ [Slack](slack.md) Integrate with the Slack chat service diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index a4491432ca..84a5a8e8c2 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -21,6 +21,7 @@ Before configuring individual OmniAuth providers there are a few global settings ``` 2. Find the section dealing with OmniAuth. The section will look similar to the following.
+ ``` ## OmniAuth settings omniauth: @@ -50,6 +51,7 @@ Before configuring individual OmniAuth providers there are a few global settings # app_secret: 'YOUR APP SECRET', # args: { scope: 'user:email' } } ``` + 3. Change `enabled` to `true`. 4. Consider the next two configuration options: `allow_single_sign_on` and `block_auto_created_users`. * `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index ac4bdefddd..9be6423f66 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -27,7 +27,7 @@ If a user is a GitLab administrator they receive all permissions. |Remove protected branches| |||✓|✓| |Edit project| |||✓|✓| |Add Deploy Keys to project| |||✓|✓| -|Confiure Project Hooks| |||✓|✓| +|Configure Project Hooks| |||✓|✓| |Switch visibility level| ||||✓| |Transfer project to another namespace| ||||✓| |Remove project| ||||✓| diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index bf9d2784af..76d83e6f3b 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -4,7 +4,7 @@ Internal projects will only be available to authenticated users. #### Public projects Public projects can be cloned **without any** authentication. -It will also be listen on the [public access directory](/public). +It will also be listed on the [public access directory](/public). **Any logged in user** will have [Guest](/help/permissions) permissions on the repository. #### Internal projects diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 9aa80af12c..6be24f0102 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -1,6 +1,7 @@ + [Backup restore](backup_restore.md) + [Cleanup](cleanup.md) + [Features](features.md) -+ [Maintenance](maintenance.md) ++ [Maintenance](maintenance.md) and self-checks + [User management](user_management.md) + [Web hooks](web_hooks.md) ++ [Import](import.md) of git repositories in bulk diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md new file mode 100644 index 0000000000..e11328dc5c --- /dev/null +++ b/doc/raketasks/import.md @@ -0,0 +1,28 @@ +### Import bare repositories into GitLab project instance + +Notes: + +* project owner will be a first admin +* groups will be created as needed +* group owner will be the first admin +* existing projects will be skipped + +How to use: + +1. copy your bare repos under git repos_path (see `config/gitlab.yml` gitlab_shell -> repos_path) +2. run the command below + +``` +bundle exec rake gitlab:import:repos RAILS_ENV=production +``` + +Example output: + +``` +Processing abcd.git + * Created abcd (abcd.git) +Processing group/xyz.git + * Created Group group (2) + * Created xyz (group/xyz.git) +[...] +``` diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 3033d8c46b..2783c4153c 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -110,32 +110,3 @@ If necessary, remove the `tmp/repo_satellites` directory and rerun the command b ``` bundle exec rake gitlab:satellites:create RAILS_ENV=production ``` - -### Import bare repositories into GitLab project instance - -Notes: - -* project owner will be a first admin -* groups will be created as needed -* group owner will be the first admin -* existing projects will be skipped - -How to use: - -1. copy your bare repos under git repos_path (see `config/gitlab.yml` gitlab_shell -> repos_path) -2. run the command below - -``` -bundle exec rake gitlab:import:repos RAILS_ENV=production -``` - -Example output: - -``` -Processing abcd.git - * Created abcd (abcd.git) -Processing group/xyz.git - * Created Group group (2) - * Created xyz (group/xyz.git) -[...] -``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 284e4e1659..e47117c7a0 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -84,8 +84,9 @@ After making the release branch new commits are cherry-picked from master. When - Push VERSION + Tag to master, merge into x-x-stable - Publish blog for new release - Tweet to blog (see below) +* 22th: release GitLab EE * 23nd: optional patch releases (x.x.1, x.x.2, etc., only if there are serious problems) -* 24-end of month: release GitLab EE and GitLab CI +* 25th: release GitLab CI # Write a blog post diff --git a/doc/update/6.0-to-6.7.md b/doc/update/6.0-to-6.8.md similarity index 84% rename from doc/update/6.0-to-6.7.md rename to doc/update/6.0-to-6.8.md index aa1b388fa9..5c71e99aa5 100644 --- a/doc/update/6.0-to-6.7.md +++ b/doc/update/6.0-to-6.8.md @@ -1,4 +1,4 @@ -# From 6.0 to 6.7 +# From 6.0 to 6.8 # In 6.1 we remove a lot of deprecated code. # You should update to 6.0 before installing 6.1 or higher so all the necessary conversions are run. @@ -33,7 +33,7 @@ sudo -u git -H git fetch --all For Gitlab Community Edition: ```bash -sudo -u git -H git checkout 6-7-stable +sudo -u git -H git checkout 6-8-stable ``` OR @@ -41,7 +41,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 6-7-stable-ee +sudo -u git -H git checkout 6-8-stable-ee ``` @@ -57,7 +57,7 @@ sudo apt-get install logrotate ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v1.9.1 # Addresses multiple critical security vulnerabilities +sudo -u git -H git checkout v1.9.3 # Addresses multiple critical security vulnerabilities ``` ### 5. Install libs, migrations, etc. @@ -90,11 +90,12 @@ sudo chmod u+rwx,g+rx,o-rwx /home/git/gitlab-satellites TIP: to see what changed in gitlab.yml.example in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 6-7-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 6-8-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-7-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-7-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-8-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-8-stable/config/unicorn.rb.example but with your settings. +* Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-8-stable/lib/support/nginx/gitlab but with your settings. * Copy rack attack middleware config ```bash diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md index 63023fd384..457433c648 100644 --- a/doc/update/6.7-to-6.8.md +++ b/doc/update/6.7-to-6.8.md @@ -62,6 +62,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS # Update init.d script sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab # Update the logrotate configuration (keep logs for 90 days instead of 52 weeks) sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab @@ -92,19 +93,12 @@ If you are using HTTPS, disable gzip as in [this commit](https://gitlab.com/gitl To improve performance, enable gzip asset compression as seen [in this commit](https://gitlab.com/gitlab-org/gitlab-ce/commit/8af94ed75505f0253823b9b2d44320fecea5b5fb). -### 6. Update Init script - -```bash -sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -sudo chmod +x /etc/init.d/gitlab -``` - -### 7. Start application +### 6. Start application sudo service gitlab start sudo service nginx restart -### 8. Check application status +### 7. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index 9a324545eb..5b9209d7df 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,9 +1,68 @@ -# Use the shell commands below to convert a MySQL GitLab database to a PostgreSQL one. +# Migrating GitLab from MySQL to Postgres + +If you are replacing MySQL with Postgres while keeping GitLab on the same +server all you need to do is to export from MySQL and import into Postgres as +described below. If you are also moving GitLab to another server, or if you are +switching to omnibus-gitlab, you may want to use a GitLab backup file. The +second part of this documents explains the procedure to do this. + +## Export from MySQL and import into Postgres + +Use this if you are keeping GitLab on the same server. ``` +sudo service gitlab stop + +# Update /home/git/gitlab/config/database.yml + git clone https://github.com/lanyrd/mysql-postgresql-converter.git cd mysql-postgresql-converter mysqldump --compatible=postgresql --default-character-set=utf8 -r databasename.mysql -u root gitlabhq_production python db_converter.py databasename.mysql databasename.psql psql -f databasename.psql -d gitlabhq_production + +sudo service gitlab start +``` + +## Converting a GitLab backup file from MySQL to Postgres + +GitLab backup files (_gitlab_backup.tar) contain a SQL dump. Using +the lanyrd database converter we can replace a MySQL database dump inside the +tar file with a Postgres database dump. This can be useful if you are moving to +another server. + +``` +# Stop GitLab +sudo service gitlab stop + +# Create the backup +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production + +# Note the filename of the backup that was created. We will call it +# TIMESTAMP_gitlab_backup.tar below. + +# Move the backup file we will convert to its own directory +sudo -u git -H mkdir -p tmp/backups/postgresql +sudo -u git -H mv tmp/backups/TIMESTAMP_gitlab_backup.tar tmp/backups/postgresql/ + +# Create a separate database dump with PostgreSQL compatibility +cd tmp/backups/postgresql +sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production + +# Clone the database converter +sudo -u git -H git clone https://github.com/lanyrd/mysql-postgresql-converter.git + +# Convert gitlabhq_production.mysql +sudo -u git -H mkdir db +sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql + +# Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar. + +# Warning: if you forget to replace TIMESTAMP below, tar will create a new file +# 'TIMESTAMP_gitlab_backup.tar' without giving an error. + +sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql + +# Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab installation. ``` diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index fd45154ac8..72a94f67b3 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -46,4 +46,8 @@ If all items are green, then congratulations upgrade is complete! You've read through the entire guide, and probably did all the steps manually. Here is a one liner for convenience, the next time you upgrade: - cd /home/git/gitlab; sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; sudo service gitlab stop; sudo -u git -H ruby script/upgrade.rb -y; sudo service gitlab start; sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```bash +cd /home/git/gitlab; sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ + sudo service gitlab stop; sudo -u git -H ruby script/upgrade.rb -y; sudo service gitlab start; \ + sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index f80891e264..5ad0c8a138 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -2,16 +2,16 @@ Project web hooks allow you to trigger an URL if new code is pushed or a new iss --- -You can configure web hook to listen for specific events like pushes, issues, merge requests. -GitLab will send POST request with data to web hook URL. -Web Hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. +You can configure web hooks to listen for specific events like pushes, issues or merge requests. +GitLab will send a POST request with data to the web hook URL. +Web hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. If you send a web hook to an SSL endpoint [the certificate will not be verified](https://gitlab.com/gitlab-org/gitlab-ce/blob/ccd617e58ea71c42b6b073e692447d0fe3c00be6/app/models/web_hook.rb#L35) since many people use self-signed certificates. --- #### Push events -Triggered when you push to the repository except pushing tags. +Triggered when you push to the repository except when pushing tags. **Request body:** @@ -84,7 +84,7 @@ Triggered when a new issue is created or an existing issue was updated/closed/re #### Merge request events -Triggered when a new merge request is created or an existing merge request was updated/merges/closed. +Triggered when a new merge request is created or an existing merge request was updated/merged/closed. **Request body:** diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature index 2d94b98c90..5832b729de 100644 --- a/features/project/forked_merge_requests.feature +++ b/features/project/forked_merge_requests.feature @@ -30,11 +30,10 @@ Feature: Project Forked Merge Requests Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" And I fill out an invalid "Merge Request On Forked Project" merge request - And I submit the merge request Then I should see validation errors @javascript Scenario: Merge request should target fork repository by default Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" - Then the target repository should be the original repository \ No newline at end of file + Then the target repository should be the original repository diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index fd9a2f01a2..a204c3e10c 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -29,3 +29,13 @@ Feature: Project Browse files Given I click on "Gemfile.lock" file in repo And I click button "edit" Then I can edit code + + @javascript + Scenario: I can see editing preview + Given I click on "Gemfile.lock" file in repo + And I click button "edit" + And I edit code + And I click link "Diff" + Then I see diff + + diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 90eb2b79c6..4a8c771dda 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -45,3 +45,20 @@ Feature: Project Wiki And I browse to that Wiki page And I click on the "Pages" button Then I should see the existing page in the pages list + + Scenario: File exists in wiki repo + Given I have an existing Wiki page with images linked on page + And I browse to wiki page with images + And I click on existing image link + Then I should see the image from wiki repo + + Scenario: Image in wiki repo shown on the page + Given I have an existing Wiki page with images linked on page + And I browse to wiki page with images + Then Image should be shown on the page + + Scenario: File does not exist in wiki repo + Given I have an existing Wiki page with images linked on page + And I browse to wiki page with images + And I click on image link + Then I should see the new wiki page form diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 394acd3fe8..706c9babce 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -25,7 +25,6 @@ class Dashboard < Spinach::FeatureSteps find("#merge_request_target_project_id").value.should == @project.id.to_s find("#merge_request_source_branch").value.should == "new_design" find("#merge_request_target_branch").value.should == "master" - find("#merge_request_title").value.should == "New design" end Given 'user with name "John Doe" joined project "Shop"' do diff --git a/features/steps/project/browse_files.rb b/features/steps/project/browse_files.rb index 069086d5ea..7cdd1101ac 100644 --- a/features/steps/project/browse_files.rb +++ b/features/steps/project/browse_files.rb @@ -41,6 +41,18 @@ class ProjectBrowseFiles < Spinach::FeatureSteps page.evaluate_script('editor.getValue()').should == "GitlabFileEditor" end + step 'I edit code' do + page.execute_script('editor.setValue("GitlabFileEditor")') + end + + step 'I click link "Diff"' do + click_link 'Diff' + end + + step 'I see diff' do + page.should have_css '.line_holder.new' + end + step 'I click on "new file" link in repo' do click_link 'new-file-link' end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index df69cb7543..3c497638d9 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -53,6 +53,7 @@ class ProjectForkedMergeRequests < Spinach::FeatureSteps find(:select, "merge_request_source_branch", {}).value.should == 'master' find(:select, "merge_request_target_branch", {}).value.should == 'stable' + click_button "Compare branches" fill_in "merge_request_title", with: "Merge Request On Forked Project" end @@ -148,29 +149,19 @@ class ProjectForkedMergeRequests < Spinach::FeatureSteps current_path.should == edit_project_merge_request_path(@project, @merge_request) page.should have_content "Edit merge request ##{@merge_request.id}" find("#merge_request_title").value.should == "Merge Request On Forked Project" - find("#merge_request_source_project_id").value.should == @forked_project.id.to_s - find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should have_content "master" - verify_commit_link(".mr_source_commit",@forked_project) - find("#merge_request_target_branch").value.should have_content "stable" - verify_commit_link(".mr_target_commit",@project) end step 'I fill out an invalid "Merge Request On Forked Project" merge request' do - #If this isn't filled in the rest of the validations won't be triggered - fill_in "merge_request_title", with: "Merge Request On Forked Project" - select "Select branch", from: "merge_request_target_branch" - find(:select, "merge_request_source_project_id", {}).value.should == @forked_project.id.to_s find(:select, "merge_request_target_project_id", {}).value.should == project.id.to_s find(:select, "merge_request_source_branch", {}).value.should == "" find(:select, "merge_request_target_branch", {}).value.should == "" + click_button "Compare branches" end step 'I should see validation errors' do - page.should have_content "Source branch can't be blank" - page.should have_content "Target branch can't be blank" + page.should have_content "You must select source and target branch" end step 'the target repository should be the original repository' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f42eb6377c..e0aec699a5 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -61,9 +61,10 @@ class ProjectMergeRequests < Spinach::FeatureSteps end step 'I submit new merge request "Wiki Feature"' do - fill_in "merge_request_title", with: "Wiki Feature" select "master", from: "merge_request_source_branch" select "notes_refactoring", from: "merge_request_target_branch" + click_button "Compare branches" + fill_in "merge_request_title", with: "Wiki Feature" click_button "Submit merge request" end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index a819ee37d7..96f2505d24 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -86,6 +86,47 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps page.should have_content @page.title end + Given 'I have an existing Wiki page with images linked on page' do + wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![image](image.jpg)", :markdown, "first commit") + @wiki_page = wiki.find_page("pictures") + end + + And 'I browse to wiki page with images' do + visit project_wiki_path(project, @wiki_page) + end + + And 'I click on existing image link' do + file = Gollum::File.new(wiki.wiki) + Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file) + Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg") + page.should have_link('image', href: "image.jpg") + click_on "image" + end + + Then 'I should see the image from wiki repo' do + url = URI.parse(current_url) + url.path.should match("wikis/image.jpg") + page.should_not have_xpath('/html') # Page should render the image which means there is no html involved + Gollum::Wiki.any_instance.unstub(:file) + Gollum::File.any_instance.unstub(:mime_type) + end + + Then 'Image should be shown on the page' do + page.should have_xpath("//img[@src=\"image.jpg\"]") + end + + And 'I click on image link' do + page.should have_link('image', href: "image.jpg") + click_on "image" + end + + Then 'I should see the new wiki page form' do + url = URI.parse(current_url) + url.path.should match("wikis/image.jpg") + page.should have_content('New Wiki Page') + page.should have_content('Editing - image.jpg') + end + def wiki @project_wiki = ProjectWiki.new(project, current_user) end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 953c6100f8..d54f9371fb 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -24,7 +24,7 @@ module API # branch (required) - The name of the branch # Example Request: # GET /projects/:id/repository/branches/:branch - get ":id/repository/branches/:branch" do + get ':id/repository/branches/:branch', requirements: { branch: /.*/ } do @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } not_found!("Branch does not exist") if @branch.nil? present @branch, with: Entities::RepoObject, project: user_project @@ -37,7 +37,9 @@ module API # branch (required) - The name of the branch # Example Request: # PUT /projects/:id/repository/branches/:branch/protect - put ":id/repository/branches/:branch/protect" do + put ':id/repository/branches/:branch/protect', + requirements: { branch: /.*/ } do + authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) @@ -55,7 +57,9 @@ module API # branch (required) - The name of the branch # Example Request: # PUT /projects/:id/repository/branches/:branch/unprotect - put ":id/repository/branches/:branch/unprotect" do + put ':id/repository/branches/:branch/unprotect', + requirements: { branch: /.*/ } do + authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) diff --git a/lib/api/users.rb b/lib/api/users.rb index ae808b6272..6ed2740c33 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -113,6 +113,45 @@ module API end end + # Get ssh keys of a specified user. Only available to admin users. + # + # Parameters: + # uid (required) - The ID of a user + # Example Request: + # GET /users/:uid/keys + get ':uid/keys' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + if user + present user.keys, with: Entities::SSHKey + else + not_found! + end + end + + # Delete existing ssh key of a specified user. Only available to admin + # users. + # + # Parameters: + # uid (required) - The ID of a user + # id (required) - SSH Key ID + # Example Request: + # DELETE /users/:uid/keys/:id + delete ':uid/keys/:id' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + if user + begin + key = user.keys.find params[:id] + key.destroy + rescue ActiveRecord::RecordNotFound + not_found! + end + else + not_found! + end + end + # Delete user. Available only for admin # # Example Request: diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 05814fc78f..28e323fe30 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -101,7 +101,7 @@ module Backup def tar_version tar_version, _ = Gitlab::Popen.popen(%W(tar --version)) - tar_version.split("\n").first + tar_version.force_encoding('locale').split("\n").first end end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 214d9824ee..6f7c4f7c90 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -10,15 +10,12 @@ module Backup Project.find_each(batch_size: 1000) do |project| print " * #{project.path_with_namespace} ... " - if project.empty_repo? - puts "[SKIPPED]".cyan - next - end - # Create namespace dir if missing FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace - if system(*%W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all), silent) + if project.empty_repo? + puts "[SKIPPED]".cyan + elsif system(*%W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all), silent) puts "[DONE]".green else puts "[FAILED]".red diff --git a/lib/gitlab/diff_parser.rb b/lib/gitlab/diff_parser.rb index fb27280c4a..14bbb32863 100644 --- a/lib/gitlab/diff_parser.rb +++ b/lib/gitlab/diff_parser.rb @@ -4,9 +4,9 @@ module Gitlab attr_reader :lines, :new_path - def initialize(diff) - @lines = diff.diff.lines.to_a - @new_path = diff.new_path + def initialize(lines, new_path = '') + @lines = lines + @new_path = new_path end def each @@ -18,10 +18,7 @@ module Gitlab lines_arr.each do |line| raw_line = line.dup - next if line.match(/^\-\-\- \/dev\/null/) - next if line.match(/^\+\+\+ \/dev\/null/) - next if line.match(/^\-\-\- a/) - next if line.match(/^\+\+\+ b/) + next if filename?(line) full_line = html_escape(line.gsub(/\n/, '')) full_line = ::Gitlab::InlineDiff.replace_markers full_line @@ -53,8 +50,17 @@ module Gitlab end end + def empty? + @lines.empty? + end + private + def filename?(line) + line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b', + '--- /tmp/diffy', '+++ /tmp/diffy') + end + def identification_type(line) if line[0] == "+" "new" diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index 1bac93378e..7f1d1cd653 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -65,7 +65,11 @@ module Gitlab end def name - auth.info.name.to_s.force_encoding("utf-8") + if auth.info.name.nil? + "#{auth.info.first_name} #{auth.info.last_name}".force_encoding('utf-8') + else + auth.info.name.to_s.force_encoding('utf-8') + end end def username diff --git a/lib/gitlab/satellite/compare_action.rb b/lib/gitlab/satellite/compare_action.rb new file mode 100644 index 0000000000..c923bb9c0f --- /dev/null +++ b/lib/gitlab/satellite/compare_action.rb @@ -0,0 +1,53 @@ +module Gitlab + module Satellite + class CompareAction < Action + def initialize(user, target_project, target_branch, source_project, source_branch) + super user, target_project + + @target_project, @target_branch = target_project, target_branch + @source_project, @source_branch = source_project, source_branch + end + + # Only show what is new in the source branch compared to the target branch, not the other way around. + # The line below with merge_base is equivalent to diff with three dots (git diff branch1...branch2) + # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B" + def diffs + in_locked_and_timed_satellite do |target_repo| + prepare_satellite!(target_repo) + update_satellite_source_and_target!(target_repo) + common_commit = target_repo.git.native(:merge_base, default_options, ["origin/#{@target_branch}", "source/#{@source_branch}"]).strip + #this method doesn't take default options + diffs = target_repo.diff(common_commit, "source/#{@source_branch}") + diffs = diffs.map { |diff| Gitlab::Git::Diff.new(diff) } + diffs + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + # Retrieve an array of commits between the source and the target + def commits + in_locked_and_timed_satellite do |target_repo| + prepare_satellite!(target_repo) + update_satellite_source_and_target!(target_repo) + commits = target_repo.commits_between("origin/#{@target_branch}", "source/#{@source_branch}") + commits = commits.map { |commit| Gitlab::Git::Commit.new(commit, nil) } + commits + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + private + + # Assumes a satellite exists that is a fresh clone of the projects repo, prepares satellite for diffs + def update_satellite_source_and_target!(target_repo) + target_repo.remote_add('source', @source_project.repository.path_to_repo) + target_repo.remote_fetch('source') + target_repo.git.checkout(default_options({b: true}), @target_branch, "origin/#{@target_branch}") + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + end + end +end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index c6e4d3351c..05123ad9c4 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -84,6 +84,7 @@ module Gitlab # Clear the working directory def clear_working_dir! repo.git.reset(hard: true) + repo.git.clean(f: true, d: true, x: true) end # Deletes all branches except the parking branch diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 853994dd67..8b4ccdfc3f 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -15,14 +15,7 @@ namespace :gitlab do end Rake::Task["db:setup"].invoke - - config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] - success = case config["adapter"] - when /^mysql/ then - Rake::Task["add_limits_mysql"].invoke - when "postgresql" then - end - + Rake::Task["add_limits_mysql"].invoke Rake::Task["db:seed_fu"].invoke rescue Gitlab::TaskAbortedByUserError puts "Quitting...".red diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 08de0f2dd5..dfc90bb333 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -1,5 +1,64 @@ namespace :gitlab do namespace :shell do + desc "GITLAB | Install or upgrade gitlab-shell" + task :install, [:tag, :repo] => :environment do |t, args| + warn_user_is_not_gitlab + + args.with_defaults(tag: "v1.9.3", repo: "https://gitlab.com/gitlab-org/gitlab-shell.git") + + user = Settings.gitlab.user + home_dir = Settings.gitlab.user_home + gitlab_url = Settings.gitlab.url + # gitlab-shell requires a / at the end of the url + gitlab_url += "/" unless gitlab_url.match(/\/$/) + repos_path = Gitlab.config.gitlab_shell.repos_path + target_dir = Gitlab.config.gitlab_shell.path + + # Clone if needed + unless File.directory?(target_dir) + sh "git clone '#{args.repo}' '#{target_dir}'" + end + + # Make sure we're on the right tag + Dir.chdir(target_dir) do + sh "git fetch origin && git reset --hard $(git describe #{args.tag} || git describe origin/#{args.tag})" + + redis_url = URI.parse(ENV['REDIS_URL'] || "redis://localhost:6379") + + config = { + user: user, + gitlab_url: gitlab_url, + http_settings: {self_signed_cert: false}.stringify_keys, + repos_path: repos_path, + auth_file: File.join(home_dir, ".ssh", "authorized_keys"), + redis: { + bin: %x{which redis-cli}.chomp, + host: redis_url.host, + port: redis_url.port, + namespace: "resque:gitlab" + }.stringify_keys, + log_level: "INFO", + audit_usernames: false + }.stringify_keys + + # Generate config.yml based on existing gitlab settings + File.open("config.yml", "w+") {|f| f.puts config.to_yaml} + + # Launch installation process + sh "bin/install" + end + + # Required for debian packaging with PKGR: Setup .ssh/environment with + # the current PATH, so that the correct ruby version gets loaded + # Requires to set "PermitUserEnvironment yes" in sshd config (should not + # be an issue since it is more than likely that there are no "normal" + # user accounts on a gitlab server). The alternative is for the admin to + # install a ruby (1.9.3+) in the global path. + File.open(File.join(home_dir, ".ssh", "environment"), "w+") do |f| + f.puts "PATH=#{ENV['PATH']}" + end + end + desc "GITLAB | Setup gitlab-shell" task setup: :environment do setup diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index 2c9b997893..9516210e20 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -8,9 +8,9 @@ namespace :gitlab do ] cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) + result = system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) - raise "#{cmd} failed!" unless $?.exitstatus.zero? + raise "#{cmd} failed!" unless result end end end diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake index 46b6451752..a1972a682d 100644 --- a/lib/tasks/migrate/add_limits_mysql.rake +++ b/lib/tasks/migrate/add_limits_mysql.rake @@ -1,14 +1,7 @@ +require Rails.root.join('db/migrate/limits_to_mysql') + desc "GITLAB | Add limits to strings in mysql database" task add_limits_mysql: :environment do puts "Adding limits to schema.rb for mysql" LimitsToMysql.new.up end - -class LimitsToMysql < ActiveRecord::Migration - def up - change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647 - change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 - change_column :snippets, :content, :text, limit: 2147483647 - change_column :notes, :st_diff, :text, limit: 2147483647 - end -end diff --git a/lib/tasks/setup.rake b/lib/tasks/setup.rake new file mode 100644 index 0000000000..93701de8f6 --- /dev/null +++ b/lib/tasks/setup.rake @@ -0,0 +1,4 @@ +desc "GITLAB | Setup gitlab db" +task :setup do + Rake::Task["gitlab:setup"].invoke +end diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..6f2e0dd090 Binary files /dev/null and b/public/apple-touch-icon-precomposed.png differ diff --git a/public/static.css b/public/static.css index aa834553a1..c6f92ac01d 100644 --- a/public/static.css +++ b/public/static.css @@ -2,7 +2,6 @@ body { color: #666; text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - sans-serif; margin:0; width: 800px; margin: auto; diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb new file mode 100644 index 0000000000..4f8a5f909d --- /dev/null +++ b/spec/finders/notes_finder_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe NotesFinder do + let(:user) { create :user } + let(:project) { create :project } + let(:note1) { create :note_on_commit, project: project } + let(:note2) { create :note_on_commit, project: project } + let(:commit) { note1.noteable } + + before do + project.team << [user, :master] + end + + describe :execute do + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + + before do + note1 + note2 + end + + it 'should find all notes' do + notes = NotesFinder.new.execute(project, user, params) + notes.size.should eq(2) + end + + it 'should raise an exception for an invalid target_type' do + params.merge!(target_type: 'invalid') + expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') + end + + it 'filters out old notes' do + note2.update_attribute(:updated_at, 2.hours.ago) + notes = NotesFinder.new.execute(project, user, params) + notes.should eq([note1]) + end + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 61c561335e..0376e0aadf 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -46,7 +46,7 @@ describe ApplicationHelper do group = create(:group) group.avatar = File.open(avatar_file_path) group.save! - group_icon(group.path).to_s.should == "/uploads/group/avatar/#{ group.id }/gitlab_logo.png" + group_icon(group.path).to_s.should match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") end it "should give default avatar_icon when no avatar is present" do @@ -63,7 +63,7 @@ describe ApplicationHelper do user = create(:user) user.avatar = File.open(avatar_file_path) user.save! - avatar_icon(user.email).to_s.should == "/uploads/user/avatar/#{ user.id }/gitlab_logo.png" + avatar_icon(user.email).to_s.should match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end it "should call gravatar_icon when no avatar is present" do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e86a60a42b..3b07504495 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -239,7 +239,7 @@ describe Notify do it_behaves_like 'an assignee email' it 'has the correct subject' do - should have_subject /#{merge_request.title} \(!#{merge_request.iid}\)/ + should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains a link to the new merge request' do @@ -275,7 +275,7 @@ describe Notify do end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(!#{merge_request.iid}\)/ + should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains the name of the previous assignee' do @@ -303,7 +303,7 @@ describe Notify do end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(!#{merge_request.iid}\)/ + should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains the new status' do @@ -426,7 +426,7 @@ describe Notify do it_behaves_like 'a note email' it 'has the correct subject' do - should have_subject /#{merge_request.title} \(!#{merge_request.iid}\)/ + should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains a link to the merge request note' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 86610c4751..a6d300b099 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -242,6 +242,67 @@ describe API::API, api: true do end end + describe 'GET /user/:uid/keys' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + get api("/users/#{user.id}/keys") + response.status.should == 401 + end + end + + context 'when authenticated' do + it 'should return 404 for non-existing user' do + get api('/users/999999/keys', admin) + response.status.should == 404 + end + + it 'should return array of ssh keys' do + user.keys << key + user.save + get api("/users/#{user.id}/keys", admin) + response.status.should == 200 + json_response.should be_an Array + json_response.first['title'].should == key.title + end + end + end + + describe 'DELETE /user/:uid/keys/:id' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + delete api("/users/#{user.id}/keys/42") + response.status.should == 401 + end + end + + context 'when authenticated' do + it 'should delete existing key' do + user.keys << key + user.save + expect { + delete api("/users/#{user.id}/keys/#{key.id}", admin) + }.to change { user.keys.count }.by(-1) + response.status.should == 200 + end + + it 'should return 404 error if user not found' do + user.keys << key + user.save + delete api("/users/999999/keys/#{key.id}", admin) + response.status.should == 404 + end + + it 'should return 404 error if key not foud' do + delete api("/users/#{user.id}/keys/42", admin) + response.status.should == 404 + end + end + end + describe "DELETE /users/:id" do before { admin }