diff options
36 files changed, 1459 insertions, 287 deletions
diff --git a/.eslintignore b/.eslintignore index 453747e14e1..a57137b4d70 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ +/coverage-javascript/ /public/ /tmp/ /vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 024e84f1ac1..82ede293a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,25 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - +- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117 - Backups do not fail anymore when using tar on annex and custom_hooks only. !5814 - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - Trim leading and trailing whitespace on project_path (Linus Thiel) - Prevent award emoji via notes for issues/MRs authored by user (barthc) +- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek) - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) - Fix Markdown styling inside reference links (Jan Zdráhal) - Fix extra space on Build sidebar on Firefox !7060 - Fail gracefully when creating merge request with non-existing branch (alexsanford) - Fix mobile layout issues in admin user overview page !7087 - Fix HipChat notifications rendering (airatshigapov, eisnerd) +- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato) - Refactor Jira service to use jira-ruby gem - Improved todos empty state - Add hover to trash icon in notes !7008 (blackst0ne) - Hides project activity tabs when features are disabled - Only show one error message for an invalid email !5905 (lycoperdon) +- Added guide describing how to upgrade PostgreSQL using Slony - Fix sidekiq stats in admin area (blackst0ne) - Added label description as tooltip to issue board list title - Created cycle analytics bundle JavaScript file @@ -28,7 +31,9 @@ Please view this file on the master branch, on stable branches it's out of date. - Do not show tooltip for active element !7105 (winniehell) - Escape ref and path for relative links !6050 (winniehell) - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) +- Fix broken issue/merge request links in JIRA comments. !6143 (Brian Kintz) - Fix filtering of milestones with quotes in title (airatshigapov) +- Fix issue boards dragging bug in Safari - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) - Update mail_room and enable sentinel support to Reply By Email (!7101) - Add task completion status in Issues and Merge Requests tabs: "X of Y tasks completed" (!6527, @gmesalazar) @@ -39,6 +44,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Optimize Event queries by removing default order - Remove duplicate links from sidebar - API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) +- Add Rake task to create/repair GitLab Shell hooks symlinks !5634 - Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld) - Replace jquery.cookie plugin with js.cookie !7085 - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4635e50c28..67c30c2424c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,6 @@ - [Technical debt](#technical-debt) - [Merge requests](#merge-requests) - [Merge request guidelines](#merge-request-guidelines) - - [Merge request description format](#merge-request-description-format) - [Contribution acceptance criteria](#contribution-acceptance-criteria) - [Changes for Stable Releases](#changes-for-stable-releases) - [Definition of done](#definition-of-done) @@ -247,13 +246,7 @@ request is as follows: 1. Fork the project into your personal space on GitLab.com 1. Create a feature branch, branch away from `master` 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code -1. Add your changes to the [CHANGELOG.md](CHANGELOG.md): - 1. If you are fixing a ~regression issue, you can add your entry to the next - patch release (e.g. `8.12.5` if current version is `8.12.4`) - 1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if - current version is `8.12.4` - 1. Please add your entry at a random place among the entries of the targeted - release +1. [Generate a changelog entry with `bin/changelog`][changelog] 1. If you are writing documentation, make sure to follow the [documentation styleguide][doc-styleguide] 1. If you have multiple commits please combine them into one commit by @@ -262,8 +255,11 @@ request is as follows: 1. Submit a merge request (MR) to the `master` branch 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you - used to achieve it, see the [merge request description format] - (#merge-request-description-format) + used to achieve it. + 1. If you are contributing code, fill in the template already provided in the + "Description" field. + 1. If you are contributing documentation, choose `Documentation` from the + "Choose a template" menu and fill in the template. 1. If the MR changes the UI it should include *Before* and *After* screenshots 1. If the MR changes CSS classes please include the list of affected pages, `grep css-class ./app -R` @@ -469,6 +465,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [contributor-covenant]: http://contributor-covenant.org [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming +[changelog]: doc/development/changelog.md "Generate a changelog entry" [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" @@ -117,7 +117,7 @@ gem 'truncato', '~> 0.7.8' gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2' # Diffs -gem 'diffy', '~> 3.0.3' +gem 'diffy', '~> 3.1.0' # Application server group :unicorn do @@ -196,7 +196,7 @@ gem 'loofah', '~> 2.0.3' gem 'licensee', '~> 8.0.0' # Protect against bruteforcing -gem 'rack-attack', '~> 4.3.1' +gem 'rack-attack', '~> 4.4.1' # Ace editor gem 'ace-rails-ap', '~> 4.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 81af1ad2dac..888fa6b2bf5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,7 +180,7 @@ GEM railties rotp (~> 2.0) diff-lcs (1.2.5) - diffy (3.0.7) + diffy (3.1.0) docile (1.1.5) doorkeeper (4.2.0) railties (>= 4.2) @@ -520,7 +520,7 @@ GEM rack (1.6.4) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (4.3.1) + rack-attack (4.4.1) rack rack-cors (0.4.0) rack-mount (0.8.3) @@ -847,7 +847,7 @@ DEPENDENCIES default_value_for (~> 3.0.0) devise (~> 4.2) devise-two-factor (~> 3.0.0) - diffy (~> 3.0.3) + diffy (~> 3.1.0) doorkeeper (~> 4.2.0) dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) @@ -929,7 +929,7 @@ DEPENDENCIES poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) pry-rails (~> 0.3.4) - rack-attack (~> 4.3.1) + rack-attack (~> 4.4.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) rails (= 4.2.7.1) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ef6833c9845..47a7e84b5c6 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -12,6 +12,10 @@ opacity: 1!important; * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; // !important to make sure no style can override this when dragging cursor: -webkit-grabbing!important; cursor: grabbing!important; @@ -45,11 +49,6 @@ .page-with-sidebar { padding-bottom: 0; } - - .issues-filters { - position: relative; - z-index: 999999; - } } .boards-app { diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 5bcf199d468..0a493b7a12b 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -237,7 +237,7 @@ class JiraService < IssueTrackerService end def resource_url(resource) - "#{Settings.gitlab['url'].chomp("/")}#{resource}" + "#{Settings.gitlab.base_url.chomp("/")}#{resource}" end def build_entity_url(entity_name, entity_id) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ff6a899b92a..c40ad06969e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -181,6 +181,7 @@ %ul %li Build traces and artifacts %li LFS objects + %li Container registry images %hr - if can? current_user, :archive_project, @project .row.prepend-top-default diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 6624d5cb427..4e41a15d9f4 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -33,7 +33,12 @@ .form-actions - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" - = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel" + .pull-right + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do + Delete + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped" - else = f.submit 'Create page', class: "btn-create btn" - = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" + .pull-right + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 4ea75dbbf0c..763c2fea39b 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -7,6 +7,3 @@ - if can?(current_user, :create_wiki, @project) = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do Edit - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do - Delete diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 233538bb488..679d6018bef 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -19,7 +19,5 @@ - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do New Page - = render 'main_links' - = render 'form' diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index f27a9002ec2..40fe53e6a8d 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,10 +1,10 @@ - project = @target_project || @project - extra_class = extra_class || '' - show_menu_above = show_menu_above || false -- selected_text = selected.try(:title) +- selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? - = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) + = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index eee0ca12af9..2fff6b0105d 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -16,7 +16,7 @@ class PostReceive post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) if post_received.project.nil? - log("Triggered hook for non-existing project with full path \"#{repo_path} \"") + log("Triggered hook for non-existing project with full path \"#{repo_path}\"") return false end @@ -25,7 +25,7 @@ class PostReceive elsif post_received.regular_project? process_project_changes(post_received) else - log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"") + log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"") false end end @@ -37,7 +37,7 @@ class PostReceive @user ||= post_received.identify(newrev) unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier} \"") + log("Triggered hook for non-existing user \"#{post_received.identifier}\"") return false end diff --git a/bin/changelog b/bin/changelog new file mode 100755 index 00000000000..a0d1ad2d730 --- /dev/null +++ b/bin/changelog @@ -0,0 +1,164 @@ +#!/usr/bin/env ruby +# +# Generate a changelog entry file in the correct location. +# +# Automatically stages the file and amends the previous commit if the `--amend` +# argument is used. + +require 'optparse' +require 'yaml' + +Options = Struct.new( + :amend, + :author, + :dry_run, + :merge_request, + :title +) + +class ChangelogOptionParser + def self.parse(argv) + options = Options.new + + parser = OptionParser.new do |opts| + opts.banner = "Usage: #{__FILE__} [options]" + + # Note: We do not provide a shorthand for this in order to match the `git + # commit` interface + opts.on('--amend', 'Amend the previous commit') do |value| + options.amend = value + end + + opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value| + options.merge_request = value + end + + opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value| + options.dry_run = value + end + + opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value| + options.author = git_user_name if value + end + + opts.on('-h', '--help', 'Print help message') do + $stdout.puts opts + exit + end + end + + parser.parse!(argv) + + # Title is everything that remains, but let's clean it up a bit + options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '') + + options + end + + def self.git_user_name + %x{git config user.name}.strip + end +end + +class ChangelogEntry + attr_reader :options + + def initialize(options) + @options = options + + assert_feature_branch! + assert_new_file! + assert_title! + + $stdout.puts "\e[32mcreate\e[0m #{file_path}" + $stdout.puts contents + + unless options.dry_run + write + amend_commit if options.amend + end + end + + def contents + YAML.dump( + 'title' => title, + 'merge_request' => options.merge_request, + 'author' => options.author + ) + end + + def write + File.write(file_path, contents) + end + + def amend_commit + %x{git add #{file_path}} + exec("git commit --amend") + end + + private + + def fail_with(message) + $stderr.puts "\e[31merror\e[0m #{message}" + exit 1 + end + + def assert_feature_branch! + return unless branch_name == 'master' + + fail_with "Create a branch first!" + end + + def assert_new_file! + return unless File.exist?(file_path) + + fail_with "#{file_path} already exists!" + end + + def assert_title! + return if options.title.length > 0 || options.amend + + fail_with "Provide a title for the changelog entry or use `--amend`" \ + " to use the title from the previous commit." + end + + def title + if options.title.empty? + last_commit_subject + else + options.title + end + end + + def last_commit_subject + %x{git log --format="%s" -1}.strip + end + + def file_path + File.join( + unreleased_path, + branch_name.gsub(/[^\w-]/, '-') << '.yml' + ) + end + + def unreleased_path + File.join('changelogs', 'unreleased').tap do |path| + path << '-ee' if ee? + end + end + + def ee? + @ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__)) + end + + def branch_name + @branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '') + end +end + +if $0 == __FILE__ + options = ChangelogOptionParser.parse(ARGV) + ChangelogEntry.new(options) +end + +# vim: ft=ruby diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 3451b68cea5..699ab6075b6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -432,7 +432,9 @@ production: &base ## Repositories settings repositories: # Paths where repositories can be stored. Give the canonicalized absolute pathname. - # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!! + # IMPORTANT: None of the path components may be symlink, because + # gitlab-shell invokes Dir.pwd inside the repository path and that results + # real path not the symlink. storages: # You must have at least a `default` storage path. default: /home/git/repositories/ diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md new file mode 100644 index 00000000000..f3c2e72341f --- /dev/null +++ b/doc/administration/raketasks/maintenance.md @@ -0,0 +1,220 @@ +# Maintenance Rake Tasks + +## Gather information about GitLab and the system it runs on + +This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:env:info +``` + +**Source Installation** + +``` +bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +Example output: + +``` +System information +System: Debian 7.8 +Current User: git +Using RVM: no +Ruby Version: 2.1.5p273 +Gem Version: 2.4.3 +Bundler Version: 1.7.6 +Rake Version: 10.3.2 +Sidekiq Version: 2.17.8 + +GitLab information +Version: 7.7.1 +Revision: 41ab9e1 +Directory: /home/git/gitlab +DB Adapter: postgresql +URL: https://gitlab.example.com +HTTP Clone URL: https://gitlab.example.com/some-project.git +SSH Clone URL: git@gitlab.example.com:some-project.git +Using LDAP: no +Using Omniauth: no + +GitLab Shell +Version: 2.4.1 +Repositories: /home/git/repositories/ +Hooks: /home/git/gitlab-shell/hooks/ +Git: /usr/bin/git +``` + +## Check GitLab configuration + +Runs the following rake tasks: + +- `gitlab:gitlab_shell:check` +- `gitlab:sidekiq:check` +- `gitlab:app:check` + +It will check that each component was setup according to the installation guide and suggest fixes for issues found. + +You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide). + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:check +``` + +**Source Installation** + +``` +bundle exec rake gitlab:check RAILS_ENV=production +``` + +NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output. + +Example output: + +``` +Checking Environment ... + +Git configured for git user? ... yes +Has python2? ... yes +python2 is supported version? ... yes + +Checking Environment ... Finished + +Checking GitLab Shell ... + +GitLab Shell version? ... OK (1.2.0) +Repo base directory exists? ... yes +Repo base directory is a symlink? ... no +Repo base owned by git:git? ... yes +Repo base access is drwxrws---? ... yes +post-receive hook up-to-date? ... yes +post-receive hooks in repos are links: ... yes + +Checking GitLab Shell ... Finished + +Checking Sidekiq ... + +Running? ... yes + +Checking Sidekiq ... Finished + +Checking GitLab ... + +Database config exists? ... yes +Database is SQLite ... no +All migrations up? ... yes +GitLab config exists? ... yes +GitLab config outdated? ... no +Log directory writable? ... yes +Tmp directory writable? ... yes +Init script exists? ... yes +Init script up-to-date? ... yes +Redis version >= 2.0.0? ... yes + +Checking GitLab ... Finished +``` + +## Rebuild authorized_keys file + +In some case it is necessary to rebuild the `authorized_keys` file. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:shell:setup +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production +``` + +``` +This will rebuild an authorized_keys file. +You will lose any data stored in authorized_keys file. +Do you want to continue (yes/no)? yes +``` + +## Clear redis cache + +If for some reason the dashboard shows wrong information you might want to +clear Redis' cache. + +**Omnibus Installation** + +``` +sudo gitlab-rake cache:clear +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +## Precompile the assets + +Sometimes during version upgrades you might end up with some wrong CSS or +missing some icons. In that case, try to precompile the assets again. + +Note that this only applies to source installations and does NOT apply to +Omnibus packages. + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at +the release of upstream GitLab. The omnibus version includes optimized versions +of those assets. Unless you are modifying the JavaScript / CSS code on your +production machine after installing the package, there should be no reason to redo +rake assets:precompile on the production machine. If you suspect that assets +have been corrupted, you should reinstall the omnibus package. + +## Tracking Deployments + +GitLab provides a Rake task that lets you track deployments in GitLab +Performance Monitoring. This Rake task simply stores the current GitLab version +in the GitLab Performance Monitoring database. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:track_deployment +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production +``` + +## Create or repair repository hooks symlink + +If the GitLab shell hooks directory location changes or another circumstance +leads to the hooks symlink becoming missing or invalid, run this Rake task +to create or repair the symlinks. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:shell:create_hooks +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production +``` diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index d8dce4388e1..6f1356ddf8f 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -107,7 +107,7 @@ downtime. Otherwise skip to the next section. 1. To see the current threads, run: ``` - apply all thread bt + thread apply all bt ``` 1. Once you're done debugging with `gdb`, be sure to detach from the process and exit: diff --git a/doc/api/projects.md b/doc/api/projects.md index 8ebac57e612..4f4b20a1874 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1139,6 +1139,7 @@ Parameters: | `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `wiki_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | +| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | ### Edit project hook @@ -1164,6 +1165,7 @@ Parameters: | `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `wiki_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | +| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | ### Delete project hook diff --git a/doc/development/README.md b/doc/development/README.md index 3f2151bbe8e..bf1f054b7d5 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -21,6 +21,7 @@ ## Process +- [Generate a changelog entry with `bin/changelog`](changelog.md) - [Code review guidelines](code_review.md) for reviewing code and having code reviewed. - [Merge request performance guidelines](merge_request_performance_guidelines.md) for ensuring merge requests do not negatively impact GitLab performance diff --git a/doc/development/changelog.md b/doc/development/changelog.md new file mode 100644 index 00000000000..d08c476e9d6 --- /dev/null +++ b/doc/development/changelog.md @@ -0,0 +1,164 @@ +# Generate a changelog entry + +This guide contains instructions for generating a changelog entry data file, as +well as information and history about our changelog process. + +## Overview + +Each bullet point, or **entry**, in our [`CHANGELOG.md`][changelog.md] file is +generated from a single data file in the [`changelogs/unreleased/`][unreleased] +(or corresponding EE) folder. The file is expected to be a [YAML] file in the +following format: + +```yaml +--- +title: "Going through change[log]s" +merge_request: 1972 +author: Ozzy Osbourne +``` + +The `merge_request` value is a reference to a merge request that adds this +entry, and the `author` key is used to give attribution to community +contributors. Both are optional. + +If you're working on the GitLab EE repository, the entry will be added to +`changelogs/unreleased-ee/` instead. + +[changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md +[unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/ +[YAML]: https://en.wikipedia.org/wiki/YAML + +## Instructions + +A `bin/changelog` script is available to generate the changelog entry file +automatically. + +Its simplest usage is to provide the value for `title`: + +```text +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' +create changelogs/unreleased/my-feature.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: +author: +``` + +The entry filename is based on the name of the current Git branch. If you run +the command above on a branch called `feature/hey-dz`, it will generate a +`changelogs/unreleased/feature-hey-dz` file. + +### Arguments + +| Argument | Shorthand | Purpose | +| ----------------- | --------- | --------------------------------------------- | +| `--amend` | | Amend the previous commit | +| `--merge-request` | `-m` | Merge Request ID | +| `--dry-run` | `-n` | Don't actually write anything, just print | +| `--git-username` | `-u` | Use Git user.name configuration as the author | +| `--help` | `-h` | Print help message | + +#### `--amend` + +You can pass the **`--amend`** argument to automatically stage the generated +file and amend it to the previous commit. + +If you use **`--amend`** and don't provide a title, it will automatically use +the "subject" of the previous commit, which is the first line of the commit +message: + +```text +$ git show --oneline +ab88683 Added an awesome new feature to GitLab + +$ bin/changelog --amend +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Added an awesome new feature to GitLab +merge_request: +author: +``` + +#### `--merge-request` or `-m` + +Use the **`--merge-request`** or **`-m`** argument to provide the +`merge_request` value: + +```text +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -m 1983 +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: 1983 +author: +``` + +#### `--dry-run` or `-n` + +Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or +committing anything: + +```text +$ bin/changelog --amend --dry-run +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Added an awesome new feature to GitLab +merge_request: +author: + +$ ls changelogs/unreleased/ +``` + +#### `--git-username` or `-u` + +Use the **`--git-username`** or **`-u`** argument to automatically fill in the +`author` value with your configured Git `user.name` value: + +```text +$ git config user.name +Jane Doe + +$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!' +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: +author: Jane Doe +``` + +## History and Reasoning + +Our `CHANGELOG` file was previously updated manually by each contributor that +felt their change warranted an entry. When two merge requests added their own +entries at the same spot in the list, it created a merge conflict in one as soon +as the other was merged. When we had dozens of merge requests fighting for the +same changelog entry location, this quickly became a major source of merge +conflicts and delays in development. + +This led us to a [boring solution] of "add your entry in a random location in +the list." This actually worked pretty well as we got further along in each +monthly release cycle, but at the start of a new cycle, when a new version +section was added and there were fewer places to "randomly" add an entry, the +conflicts became a problem again until we had a sufficient number of entries. + +On top of all this, it created an entirely different headache for [release managers] +when they cherry-picked a commit into a stable branch for a patch release. If +the commit included an entry in the `CHANGELOG`, it would include the entire +changelog for the latest version in `master`, so the release manager would have +to manually remove the later entries. They often would have had to do this +multiple times per patch release. This was compounded when we had to release +multiple patches at once due to a security issue. + +We needed to automate all of this manual work. So we [started brainstorming]. +After much discussion we settled on the current solution of one file per entry, +and then compiling the entries into the overall `CHANGELOG.md` file during the +[release process]. + +[boring solution]: https://about.gitlab.com/handbook/#boring-solutions +[release managers]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/release-manager.md +[started brainstorming]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17826 +[release process]: https://gitlab.com/gitlab-org/release-tools + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/testing.md b/doc/development/testing.md index 8e91ac5e3ba..b0b26ccf57a 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -132,6 +132,42 @@ Adding new Spinach scenarios is acceptable _only if_ the new scenario requires no more than one new `step` definition. If more than that is required, the test should be re-implemented using RSpec instead. +## Testing Rake Tasks + +To make testing Rake tasks a little easier, there is a helper that can be included +in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use +`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures +a few other things to make testing Rake tasks easier. + +At a minimum, requiring the Rake helper will redirect `stdout`, include the +runtime task helpers, and include the `RakeHelpers` Spec support module. + +The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make +executing tasks simple. See `spec/support/rake_helpers.rb` for all available +methods. + +Example: + +```ruby +require 'rake_helper' + +describe 'gitlab:shell rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/shell' + + stub_warn_user_is_not_gitlab + end + + describe 'install task' do + it 'invokes create_hooks task' do + expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) + + run_rake_task('gitlab:shell:install') + end + end +end +``` + --- [Return to Development documentation](README.md) diff --git a/doc/integration/jira.md b/doc/integration/jira.md index cf1557ddc44..2e31fd994de 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -135,7 +135,7 @@ password as they will be needed when configuring GitLab in the next section. JIRA configuration in GitLab is done via a project's **Services**. -#### GitLab 13.0 with JIRA v1000.x +#### GitLab 8.13.0 with JIRA v1000.x To enable JIRA integration in a project, navigate to the project's and open the context menu clicking on the top right gear icon, then go to @@ -160,7 +160,7 @@ with the linked JIRA project. #### GitLab 6.x-7.7 with JIRA v6.x -_**Note:** GitLab versions 13.0 and up contain various integration improvements. +_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. We strongly recommend upgrading._ In `gitlab.yml` enable the JIRA issue tracker section by diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 315cb56a089..266aeb7d60e 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -1,188 +1,3 @@ -# Maintenance +# Maintenance Rake Tasks -## Gather information about GitLab and the system it runs on - -This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues. - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:env:info - -# installation from source -bundle exec rake gitlab:env:info RAILS_ENV=production -``` - -Example output: - -``` -System information -System: Debian 7.8 -Current User: git -Using RVM: no -Ruby Version: 2.1.5p273 -Gem Version: 2.4.3 -Bundler Version: 1.7.6 -Rake Version: 10.3.2 -Sidekiq Version: 2.17.8 - -GitLab information -Version: 7.7.1 -Revision: 41ab9e1 -Directory: /home/git/gitlab -DB Adapter: postgresql -URL: https://gitlab.example.com -HTTP Clone URL: https://gitlab.example.com/some-project.git -SSH Clone URL: git@gitlab.example.com:some-project.git -Using LDAP: no -Using Omniauth: no - -GitLab Shell -Version: 2.4.1 -Repositories: /home/git/repositories/ -Hooks: /home/git/gitlab-shell/hooks/ -Git: /usr/bin/git -``` - -## Check GitLab configuration - -Runs the following rake tasks: - -- `gitlab:gitlab_shell:check` -- `gitlab:sidekiq:check` -- `gitlab:app:check` - -It will check that each component was setup according to the installation guide and suggest fixes for issues found. - -You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide). - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:check - -# installation from source -bundle exec rake gitlab:check RAILS_ENV=production -``` - -NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output. - -Example output: - -``` -Checking Environment ... - -Git configured for git user? ... yes -Has python2? ... yes -python2 is supported version? ... yes - -Checking Environment ... Finished - -Checking GitLab Shell ... - -GitLab Shell version? ... OK (1.2.0) -Repo base directory exists? ... yes -Repo base directory is a symlink? ... no -Repo base owned by git:git? ... yes -Repo base access is drwxrws---? ... yes -post-receive hook up-to-date? ... yes -post-receive hooks in repos are links: ... yes - -Checking GitLab Shell ... Finished - -Checking Sidekiq ... - -Running? ... yes - -Checking Sidekiq ... Finished - -Checking GitLab ... - -Database config exists? ... yes -Database is SQLite ... no -All migrations up? ... yes -GitLab config exists? ... yes -GitLab config outdated? ... no -Log directory writable? ... yes -Tmp directory writable? ... yes -Init script exists? ... yes -Init script up-to-date? ... yes -Redis version >= 2.0.0? ... yes - -Checking GitLab ... Finished -``` - -## Rebuild authorized_keys file - -In some case it is necessary to rebuild the `authorized_keys` file. - -For Omnibus-packages: -``` -sudo gitlab-rake gitlab:shell:setup -``` - -For installations from source: -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production -``` - -``` -This will rebuild an authorized_keys file. -You will lose any data stored in authorized_keys file. -Do you want to continue (yes/no)? yes -``` - -## Clear redis cache - -If for some reason the dashboard shows wrong information you might want to -clear Redis' cache. - -For Omnibus-packages: -``` -sudo gitlab-rake cache:clear -``` - -For installations from source: -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production -``` - -## Precompile the assets - -Sometimes during version upgrades you might end up with some wrong CSS or -missing some icons. In that case, try to precompile the assets again. - -Note that this only applies to source installations and does NOT apply to -omnibus packages. - -For installations from source: -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production -``` - -For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at -the release of upstream GitLab. The omnibus version includes optimized versions -of those assets. Unless you are modifying the JavaScript / CSS code on your -production machine after installing the package, there should be no reason to redo -rake assets:precompile on the production machine. If you suspect that assets -have been corrupted, you should reinstall the omnibus package. - -## Tracking Deployments - -GitLab provides a Rake task that lets you track deployments in GitLab -Performance Monitoring. This Rake task simply stores the current GitLab version -in the GitLab Performance Monitoring database. - -For Omnibus-packages: - -``` -sudo gitlab-rake gitlab:track_deployment -``` - -For installations from source: - -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production -``` +This document was moved to [administration/raketasks/maintenance](../administration/raketasks/maintenance.md). diff --git a/doc/update/README.md b/doc/update/README.md index 975d72164b4..837b31abb97 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -85,6 +85,8 @@ possible. - [MySQL installation guide](../install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) +- [Upgrading PostgreSQL Using Slony](upgrading_postgresql_using_slony.md), for + upgrading a PostgreSQL database with minimal downtime. [omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md new file mode 100644 index 00000000000..f009906256e --- /dev/null +++ b/doc/update/upgrading_postgresql_using_slony.md @@ -0,0 +1,482 @@ +# Upgrading PostgreSQL Using Slony + +This guide describes the steps one can take to upgrade their PostgreSQL database +to the latest version without the need for hours of downtime. This guide assumes +you have two database servers: one database server running an older version of +PostgreSQL (e.g. 9.2.18) and one server running a newer version (e.g. 9.6.0). + +For this process we'll use a PostgreSQL replication tool called +["Slony"](http://www.slony.info/). Slony allows replication between different +PostgreSQL versions and as such can be used to upgrade a cluster with a minimal +amount of downtime. + +In various places we'll refer to the user `gitlab-psql`. This user should be the +user used to run the various PostgreSQL OS processes. If you're using a +different user (e.g. `postgres`) you should replace `gitlab-psql` with the name +of said user. This guide also assumes your database is called +`gitlabhq_production`. If you happen to use a different database name you should +change this accordingly. + +## Database Dumps + +Slony only replicates data and not any schema changes. As a result we must +ensure that all databases have the same database structure. + +To do so we'll generate a dump of our current database. This dump will only +contain the structure, not any data. To generate this dump run the following +command on your active database server: + +```bash +sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql -p 5432 -U gitlab-psql -s -f /tmp/structure.sql gitlabhq_production +``` + +If you're not using GitLab's Omnibus package you may have to adjust the paths to +`pg_dump` and the PostgreSQL installation directory to match the paths of your +configuration. + +Once the structure dump is generated we also need to generate a dump for the +`schema_migrations` table. This table doesn't have any primary keys and as such +can't be replicated easily by Slony. To generate this dump run the following +command on your active database server: + +```bash +sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql/ -p 5432 -U gitlab-psql -a -t schema_migrations -f /tmp/migrations.sql gitlabhq_production +``` + +Next we'll need to move these files somewhere accessible by the new database +server. The easiest way is to simply download these files to your local system: + +```bash +scp your-user@production-database-host:/tmp/*.sql /tmp +``` + +This will copy all the SQL files located in `/tmp` to your local system's +`/tmp` directory. Once copied you can safely remove the files from the database +server. + +## Installing Slony + +Slony will be used to upgrade the database without requiring long downtimes. +Slony can be downloaded from http://www.slony.info/. If you have installed +PostgreSQL using your operating system's package manager you may also be able to +install Slony using said package manager. + +When compiling Slony from source you *must* use the following commands to do so: + +```bash +./configure --prefix=/path/to/installation/directory --with-perltools --with-pgconfigdir=/path/to/directory/containing/pg_config/bin +make +make install +``` + +Omnibus users can use the following commands: + +```bash +./configure --prefix=/opt/gitlab/embedded --with-perltools --with-pgconfigdir=/opt/gitlab/embedded/bin +make +make install +``` + +This assumes you have installed GitLab into /opt/gitlab. + +To test if Slony is installed properly, run the following commands: + +```bash +test -f /opt/gitlab/embedded/bin/slonik && echo 'Slony installed' || echo 'Slony not installed' +test -f /opt/gitlab/embedded/bin/slonik_init_cluster && echo 'Slony Perl tools are available' || echo 'Slony Perl tools are not available' +/opt/gitlab/embedded/bin/slonik -v +``` + +This assumes Slony was installed to `/opt/gitlab/embedded`. If Slony was +installed properly the output of these commands will be (the mentioned "slonik" +version may be different): + +``` +Slony installed +Slony Perl tools are available +slonik version 2.2.5 +``` + +## Slony User + +Next we must set up a PostgreSQL user that Slony can use to replicate your +database. To do so, log in to your production database using `psql` using a +super user account. Once done run the following SQL queries: + +```sql +CREATE ROLE slony WITH SUPERUSER LOGIN REPLICATION ENCRYPTED PASSWORD 'password string here'; +ALTER ROLE slony SET statement_timeout TO 0; +``` + +Make sure you replace "password string here" with the actual password for the +user. A password is *required*. This user must be created on _both_ the old and +new database server using the same password. + +Once the user has been created make sure you note down the password as we will +need it later on. + +## Configuring Slony + +Now we can finally start configuring Slony. Slony uses a configuration file for +most of the work so we'll need to set this one up. This configuration file +specifies where to put log files, how Slony should connect to the databases, +etc. + +First we'll need to create some required directories and set the correct +permissions. To do so, run the following commands on both the old and new +database server: + +```bash +sudo mkdir -p /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony +sudo chown gitlab-psql:root /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony +``` + +Here `gitlab-psql` is the user used to run the PostgreSQL database processes. If +you're using a different user you should replace this with the name of said +user. + +Now that the directories are in place we can create the configuration file. For +this we can use the following template: + +```perl +if ($ENV{"SLONYNODES"}) { + require $ENV{"SLONYNODES"}; +} else { + $CLUSTER_NAME = 'slony_replication'; + $LOGDIR = '/var/log/gitlab/slony'; + $MASTERNODE = 1; + $DEBUGLEVEL = 2; + + add_node(host => 'OLD_HOST', dbname => 'gitlabhq_production', port =>5432, + user=>'slony', password=>'SLONY_PASSWORD', node=>1); + + add_node(host => 'NEW_HOST', dbname => 'gitlabhq_production', port =>5432, + user=>'slony', password=>'SLONY_PASSWORD', node=>2, parent=>1 ); +} + +$SLONY_SETS = { + "set1" => { + "set_id" => 1, + "table_id" => 1, + "sequence_id" => 1, + "pkeyedtables" => [ + TABLES + ], + }, +}; + +if ($ENV{"SLONYSET"}) { + require $ENV{"SLONYSET"}; +} + +# Please do not add or change anything below this point. +1; +``` + +In this configuration file you should replace a few placeholders before you can +use it. The following placeholders should be replaced: + +* `OLD_HOST`: the address of the old database server. +* `NEW_HOST`: the address of the new database server. +* `SLONY_PASSWORD`: the password of the Slony user created earlier. +* `TABLES`: the tables to replicate. + +The list of tables to replicate can be generated by running the following +command on your old PostgreSQL database: + +``` +sudo gitlab-psql gitlabhq_production -c "select concat('\"', schemaname, '.', tablename, '\",') from pg_catalog.pg_tables where schemaname = 'public' and tableowner = 'gitlab' and tablename != 'schema_migrations' order by tablename asc;" -t +``` + +If you're not using Omnibus you should replace `gitlab-psql` with the +appropriate path to the `psql` executable. + +The above command outputs a list of tables in a format that can be copy-pasted +directly into the above configuration file. Make sure to _replace_ `TABLES` with +this output, don't just append it below it. Once done you'll end up with +something like this: + +```perl +"pkeyedtables" => [ + "public.abuse_reports", + "public.appearances", + "public.application_settings", + ... more rows here ... +] +``` + +Once you have the configuration file generated you must install it on both the +old and new database. To do so, place it in +`/var/opt/gitlab/postgresql/slony/slon_tools.conf` (for which we created the +directory earlier on). + +Now that the configuration file is in place we can _finally_ start replicating +our database. First we must set up the schema in our new database. To do so make +sure that the SQL files we generated earlier can be found in the `/tmp` +directory of the new server. Once these files are in place start a `psql` +session on this server: + +``` +sudo gitlab-psql gitlabhq_production +``` + +Now run the following commands: + +``` +\i /tmp/structure.sql +\i /tmp/migrations.sql +``` + +To verify if the structure is in place close the session, start it again, then +run `\d`. If all went well you should see output along the lines of the +following: + +``` + List of relations + Schema | Name | Type | Owner +--------+---------------------------------------------+----------+------------- + public | abuse_reports | table | gitlab + public | abuse_reports_id_seq | sequence | gitlab + public | appearances | table | gitlab + public | appearances_id_seq | sequence | gitlab + public | application_settings | table | gitlab + public | application_settings_id_seq | sequence | gitlab + public | approvals | table | gitlab + ... more rows here ... +``` + +Now we can initialize the required tables and what not that Slony will use for +its replication process. To do so, run the following on the old database: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_init_cluster --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik +``` + +If all went well this will produce something along the lines of: + +``` +<stdin>:10: Set up replication nodes +<stdin>:13: Next: configure paths for each node/origin +<stdin>:16: Replication nodes prepared +<stdin>:17: Please start a slon replication daemon for each node +``` + +Next we need to start a replication node on every server. To do so, run the +following on the old database: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf +``` + +If all went well this will produce output such as: + + +``` +Invoke slon for node 1 - /opt/gitlab/embedded/bin/slon -p /var/run/slony1/slony_replication_node1.pid -s 1000 -d2 slony_replication 'host=192.168.0.7 dbname=gitlabhq_production user=slony port=5432 password=hieng8ezohHuCeiqu0leeghai4aeyahp' > /var/log/gitlab/slony/node1/gitlabhq_production-2016-10-06.log 2>&1 & +Slon successfully started for cluster slony_replication, node node1 +PID [26740] +Start the watchdog process as well... +``` + +Next we need to run the following command on the _new_ database server: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf +``` + +This will produce similar output if all went well. + +Next we need to tell the new database server what it should replicate. This can +be done by running the following command on the _new_ database server: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_create_set 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik +``` + +This should produce output along the lines of the following: + +``` +<stdin>:11: Subscription set 1 (set1) created +<stdin>:12: Adding tables to the subscription set +<stdin>:16: Add primary keyed table public.abuse_reports +<stdin>:20: Add primary keyed table public.appearances +<stdin>:24: Add primary keyed table public.application_settings +... more rows here ... +<stdin>:327: Adding sequences to the subscription set +<stdin>:328: All tables added +``` + +Finally we can start the replication process by running the following on the +_new_ database server: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_subscribe_set 1 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik +``` + +This should produce the following output: + +``` +<stdin>:6: Subscribed nodes to set 1 +``` + +At this point the new database server will start replicating the data of the old +database server. This process can take anywhere from a few minutes to hours, if +not days. Unfortunately Slony itself doesn't really provide a way of knowing +when the two databases are in sync. To get an estimate of the progress you can +use the following shell script: + +``` +#!/usr/bin/env bash + +set -e + +user='slony' +pass='SLONY_PASSWORD' + +function main { + while : + do + local source + local target + + source=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h OLD_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A) + target=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h NEW_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A) + + echo "$(date): ${target} of ${source}" >> progress.log + echo "$(date): ${target} of ${source}" + + sleep 60 + done +} + +main +``` + +This script will compare the sizes of the old and new database every minute and +print the result to STDOUT as well as logging it to a file. Make sure to replace +`SLONY_PASSWORD`, `OLD_HOST`, and `NEW_HOST` with the correct values. + +## Stopping Replication + +At some point the two databases are in sync. Once this is the case you'll need +to plan for a few minutes of downtime. This small downtime window is used to +stop the replication process, remove any Slony data from both databases, restart +GitLab so it can use the new database, etc. + +First, let's stop all of GitLab. Omnibus users can do so by running the +following on their GitLab server(s): + +``` +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq +sudo gitlab-ctl stop mailroom +``` + +If you have any other processes that use PostgreSQL you should also stop those. + +Once everything has been stopped you should update any configuration settings, +DNS records, etc so they all point to the new database. + +Once the settings have been taken care of we need to stop the replication +process. It's crucial that no new data is written to the databases at this point +as this data will be lost. + +To stop replication, run the following on both database servers: + +```bash +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_kill --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf +``` + +This will stop all the Slony processes on the host the command was executed on. + +## Resetting Sequences + +The above setup does not replicate database sequences, as such these must be +reset manually in the target database. You can use the following script for +this: + +```bash +#!/usr/bin/env bash +set -e + +function main { + local fix_sequences + local fix_owners + + fix_sequences='/tmp/fix_sequences.sql' + fix_owners='/tmp/fix_owners.sql' + + # The SQL queries were taken from + # https://wiki.postgresql.org/wiki/Fixing_Sequences + sudo gitlab-psql gitlabhq_production -t -c " + SELECT 'ALTER SEQUENCE '|| quote_ident(MIN(schema_name)) ||'.'|| quote_ident(MIN(seq_name)) + ||' OWNED BY '|| quote_ident(MIN(TABLE_NAME)) ||'.'|| quote_ident(MIN(column_name)) ||';' + FROM ( + SELECT + n.nspname AS schema_name, + c.relname AS TABLE_NAME, + a.attname AS column_name, + SUBSTRING(d.adsrc FROM E'^nextval\\(''([^'']*)''(?:::text|::regclass)?\\)') AS seq_name + FROM pg_class c + JOIN pg_attribute a ON (c.oid=a.attrelid) + JOIN pg_attrdef d ON (a.attrelid=d.adrelid AND a.attnum=d.adnum) + JOIN pg_namespace n ON (c.relnamespace=n.oid) + WHERE has_schema_privilege(n.oid,'USAGE') + AND n.nspname NOT LIKE 'pg!_%' escape '!' + AND has_table_privilege(c.oid,'SELECT') + AND (NOT a.attisdropped) + AND d.adsrc ~ '^nextval' + ) seq + GROUP BY seq_name HAVING COUNT(*)=1; + " > "${fix_owners}" + + sudo gitlab-psql gitlabhq_production -t -c " + SELECT 'SELECT SETVAL(' || + quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) || + ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' || + quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';' + FROM pg_class AS S, + pg_depend AS D, + pg_class AS T, + pg_attribute AS C, + pg_tables AS PGT + WHERE S.relkind = 'S' + AND S.oid = D.objid + AND D.refobjid = T.oid + AND D.refobjid = C.attrelid + AND D.refobjsubid = C.attnum + AND T.relname = PGT.tablename + ORDER BY S.relname; + " > "${fix_sequences}" + + sudo gitlab-psql gitlabhq_production -f "${fix_owners}" + sudo gitlab-psql gitlabhq_production -f "${fix_sequences}" + + rm "${fix_owners}" "${fix_sequences}" +} + +main +``` + +Upload this script to the _target_ server and execute it as follows: + +```bash +bash path/to/the/script/above.sh +``` + +This will correct the ownership of sequences and reset the next value for the +`id` column to the next available value. + +## Removing Slony + +Next we need to remove all Slony related data. To do so, run the following +command on the _target_ server: + +```bash +sudo gitlab-psql gitlabhq_production -c "DROP SCHEMA _slony_replication CASCADE;" +``` + +Once done you can safely remove any Slony related files (e.g. the log +directory), and uninstall Slony if desired. At this point you can start your +GitLab instance again and if all went well it should be using your new database +server. diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb index 9c94dc70df0..3c5bf44c538 100644 --- a/features/steps/dashboard/help.rb +++ b/features/steps/dashboard/help.rb @@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps end step 'I visit the "Rake Tasks" help page' do - visit help_page_path("raketasks/maintenance") + visit help_page_path("administration/raketasks/maintenance") end step 'I should see "Rake Tasks" page markdown rendered' do diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 14f5be3b5f6..dd93a85dc54 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -47,7 +47,8 @@ module API :build_events, :pipeline_events, :wiki_page_events, - :enable_ssl_verification + :enable_ssl_verification, + :token ] @hook = user_project.hooks.new(attrs) @@ -82,7 +83,8 @@ module API :build_events, :pipeline_events, :wiki_page_events, - :enable_ssl_verification + :enable_ssl_verification, + :token ] if @hook.update_attributes attrs diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 210899882b4..58761a129d4 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -63,11 +63,11 @@ namespace :gitlab do # Launch installation process system(*%W(bin/install) + repository_storage_paths_args) - - # (Re)create hooks - system(*%W(bin/create-hooks) + repository_storage_paths_args) end + # (Re)create hooks + Rake::Task['gitlab:shell:create_hooks'].invoke + # 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 @@ -102,6 +102,15 @@ namespace :gitlab do end end end + + desc 'Create or repair repository hooks symlink' + task create_hooks: :environment do + warn_user_is_not_gitlab + + puts 'Creating/Repairing hooks symlinks for all repositories' + system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args) + puts 'done'.color(:green) + end end def setup diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb new file mode 100644 index 00000000000..da167dc570f --- /dev/null +++ b/spec/bin/changelog_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +load File.expand_path('../../bin/changelog', __dir__) + +describe 'bin/changelog' do + describe ChangelogOptionParser do + it 'parses --ammend' do + options = described_class.parse(%w[foo bar --amend]) + + expect(options.amend).to eq true + end + + it 'parses --merge-request' do + options = described_class.parse(%w[foo --merge-request 1234 bar]) + + expect(options.merge_request).to eq 1234 + end + + it 'parses -m' do + options = described_class.parse(%w[foo -m 4321 bar]) + + expect(options.merge_request).to eq 4321 + end + + it 'parses --dry-run' do + options = described_class.parse(%w[foo --dry-run bar]) + + expect(options.dry_run).to eq true + end + + it 'parses -n' do + options = described_class.parse(%w[foo -n bar]) + + expect(options.dry_run).to eq true + end + + it 'parses --git-username' do + allow(described_class).to receive(:git_user_name).and_return('Jane Doe') + options = described_class.parse(%w[foo --git-username bar]) + + expect(options.author).to eq 'Jane Doe' + end + + it 'parses -u' do + allow(described_class).to receive(:git_user_name).and_return('John Smith') + options = described_class.parse(%w[foo -u bar]) + + expect(options.author).to eq 'John Smith' + end + + it 'parses -h' do + expect do + $stdout = StringIO.new + + described_class.parse(%w[foo -h bar]) + end.to raise_error(SystemExit) + end + + it 'assigns title' do + options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend]) + + expect(options.title).to eq 'foo bar baz' + end + end +end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 88e1549a22b..9dfa5d1de19 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::None.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') expect(page).to have_css('.issue', count: 1) end @@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 0) end @@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 1) end @@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 0) end end @@ -55,6 +59,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) expect(page).to have_css('.issue', count: 1) end @@ -70,6 +75,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) expect(page).to have_css('.issue', count: 1) end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index a9f637147d1..a3e9adae4e2 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +include Gitlab::Routing.url_helpers describe JiraService, models: true do describe "Associations" do @@ -66,6 +67,27 @@ describe JiraService, models: true do ).once end + it "references the GitLab commit/merge request" do + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).to have_requested(:post, @comment_url).with( + body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ + ).once + end + + it "references the GitLab commit/merge request (relative URL)" do + stub_config_setting(relative_url_root: '/gitlab') + stub_config_setting(url: Settings.send(:build_gitlab_url)) + + Project.default_url_options[:script_name] = "/gitlab" + + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).to have_requested(:post, @comment_url).with( + body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ + ).once + end + it "calls the api with jira_issue_transition_id" do @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb new file mode 100644 index 00000000000..9b5b4bf9fea --- /dev/null +++ b/spec/rake_helper.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +require 'rake' + +RSpec.configure do |config| + config.include RakeHelpers + + # Redirect stdout so specs don't have so much noise + config.before(:all) do + $stdout = StringIO.new + + Rake.application.rake_require 'tasks/gitlab/task_helpers' + Rake::Task.define_task :environment + end + + # Reset stdout + config.after(:all) do + $stdout = STDOUT + end +end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index cfcdcad74cd..5f39329a1b8 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -88,6 +88,7 @@ describe API::API, 'ProjectHooks', api: true do expect do post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true end.to change {project.hooks.count}.by(1) + expect(response).to have_http_status(201) expect(json_response['url']).to eq('http://example.com') expect(json_response['issues_events']).to eq(true) @@ -99,6 +100,24 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(false) expect(json_response['enable_ssl_verification']).to eq(true) + expect(json_response).not_to include('token') + end + + it "adds the token without including it in the response" do + token = "secret token" + + expect do + post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token + end.to change {project.hooks.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response["url"]).to eq("http://example.com") + expect(json_response).not_to include("token") + + hook = project.hooks.find(json_response["id"]) + + expect(hook.url).to eq("http://example.com") + expect(hook.token).to eq(token) end it "returns a 400 error if url not given" do @@ -129,6 +148,19 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end + it "adds the token without including it in the response" do + token = "secret token" + + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token + + expect(response).to have_http_status(200) + expect(json_response["url"]).to eq("http://example.org") + expect(json_response).not_to include("token") + + expect(hook.reload.url).to eq("http://example.org") + expect(hook.reload.token).to eq(token) + end + it "returns 404 error if hook id not found" do put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' expect(response).to have_http_status(404) diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb new file mode 100644 index 00000000000..52d80c69835 --- /dev/null +++ b/spec/support/rake_helpers.rb @@ -0,0 +1,10 @@ +module RakeHelpers + def run_rake_task(task_name) + Rake::Task[task_name].reenable + Rake.application.invoke_task task_name + end + + def stub_warn_user_is_not_gitlab + allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab) + end +end diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb new file mode 100644 index 00000000000..226d34fe2c9 --- /dev/null +++ b/spec/tasks/gitlab/shell_rake_spec.rb @@ -0,0 +1,26 @@ +require 'rake_helper' + +describe 'gitlab:shell rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/shell' + + stub_warn_user_is_not_gitlab + end + + describe 'install task' do + it 'invokes create_hooks task' do + expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) + + run_rake_task('gitlab:shell:install') + end + end + + describe 'create_hooks task' do + it 'calls gitlab-shell bin/create_hooks' do + expect_any_instance_of(Object).to receive(:system) + .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args) + + run_rake_task('gitlab:shell:create_hooks') + end + end +end diff --git a/vendor/assets/javascripts/Sortable.js b/vendor/assets/javascripts/Sortable.js index eca7c5012b2..f9e57bcb855 100644 --- a/vendor/assets/javascripts/Sortable.js +++ b/vendor/assets/javascripts/Sortable.js @@ -4,8 +4,7 @@ * @license MIT */ - -(function (factory) { +(function sortableModule(factory) { "use strict"; if (typeof define === "function" && define.amd) { @@ -15,15 +14,22 @@ module.exports = factory(); } else if (typeof Package !== "undefined") { + //noinspection JSUnresolvedVariable Sortable = factory(); // export for Meteor.js } else { /* jshint sub:true */ window["Sortable"] = factory(); } -})(function () { +})(function sortableFactory() { "use strict"; + if (typeof window == "undefined" || !window.document) { + return function sortableError() { + throw new Error("Sortable.js requires a window with a document"); + }; + } + var dragEl, parentEl, ghostEl, @@ -33,6 +39,7 @@ scrollEl, scrollParentEl, + scrollCustomFn, lastEl, lastCSS, @@ -42,6 +49,8 @@ newIndex, activeGroup, + putSortable, + autoScroll = {}, tapEvt, @@ -58,8 +67,15 @@ document = win.document, parseInt = win.parseInt, + $ = win.jQuery || win.Zepto, + Polymer = win.Polymer, + supportDraggable = !!('draggable' in document.createElement('div')), supportCssPointerEvents = (function (el) { + // false when IE11 + if (!!navigator.userAgent.match(/Trident.*rv[ :]?11\./)) { + return false; + } el = document.createElement('x'); el.style.cssText = 'pointer-events:auto'; return el.style.pointerEvents === 'auto'; @@ -88,13 +104,17 @@ winHeight = window.innerHeight, vx, - vy + vy, + + scrollOffsetX, + scrollOffsetY ; // Delect scrollEl if (scrollParentEl !== rootEl) { scrollEl = options.scroll; scrollParentEl = rootEl; + scrollCustomFn = options.scrollFn; if (scrollEl === true) { scrollEl = rootEl; @@ -136,11 +156,18 @@ if (el) { autoScroll.pid = setInterval(function () { + scrollOffsetY = vy ? vy * speed : 0; + scrollOffsetX = vx ? vx * speed : 0; + + if ('function' === typeof(scrollCustomFn)) { + return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt); + } + if (el === win) { - win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed); + win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); } else { - vy && (el.scrollTop += vy * speed); - vx && (el.scrollLeft += vx * speed); + el.scrollTop += scrollOffsetY; + el.scrollLeft += scrollOffsetX; } }, 24); } @@ -149,19 +176,39 @@ }, 30), _prepareGroup = function (options) { - var group = options.group; + function toFn(value, pull) { + if (value === void 0 || value === true) { + value = group.name; + } - if (!group || typeof group != 'object') { - group = options.group = {name: group}; + if (typeof value === 'function') { + return value; + } else { + return function (to, from) { + var fromGroup = from.options.group.name; + + return pull + ? value + : value && (value.join + ? value.indexOf(fromGroup) > -1 + : (fromGroup == value) + ); + }; + } } - ['pull', 'put'].forEach(function (key) { - if (!(key in group)) { - group[key] = true; - } - }); + var group = {}; + var originalGroup = options.group; + + if (!originalGroup || typeof originalGroup != 'object') { + originalGroup = {name: originalGroup}; + } - options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' '; + group.name = originalGroup.name; + group.checkPull = toFn(originalGroup.pull, true); + group.checkPut = toFn(originalGroup.put); + + options.group = group; } ; @@ -198,6 +245,7 @@ draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', ignore: 'a, img', filter: null, animation: 0, @@ -211,7 +259,8 @@ forceFallback: false, fallbackClass: 'sortable-fallback', fallbackOnBody: false, - fallbackTolerance: 0 + fallbackTolerance: 0, + fallbackOffset: {x: 0, y: 0} }; @@ -224,7 +273,7 @@ // Bind all private methods for (var fn in this) { - if (fn.charAt(0) === '_') { + if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { this[fn] = this[fn].bind(this); } } @@ -258,7 +307,7 @@ type = evt.type, touch = evt.touches && evt.touches[0], target = (touch || evt).target, - originalTarget = target, + originalTarget = evt.target.shadowRoot && evt.path[0] || target, filter = options.filter, startIndex; @@ -271,13 +320,13 @@ return; // only left button or enabled } - target = _closest(target, options.draggable, el); - - if (!target) { + if (options.handle && !_closest(originalTarget, options.handle, el)) { return; } - if (options.handle && !_closest(originalTarget, options.handle, el)) { + target = _closest(target, options.draggable, el); + + if (!target) { return; } @@ -332,16 +381,18 @@ this._lastX = (touch || evt).clientX; this._lastY = (touch || evt).clientY; + dragEl.style['will-change'] = 'transform'; + dragStartFn = function () { // Delayed drag has been triggered // we can re-enable the events: touchmove/mousemove _this._disableDelayedDrag(); // Make the element draggable - dragEl.draggable = true; + dragEl.draggable = _this.nativeDraggable; // Chosen item - _toggleClass(dragEl, _this.options.chosenClass, true); + _toggleClass(dragEl, options.chosenClass, true); // Bind the events: dragstart/dragend _this._triggerDragStart(touch); @@ -408,7 +459,10 @@ try { if (document.selection) { - document.selection.empty(); + // Timeout neccessary for IE9 + setTimeout(function () { + document.selection.empty(); + }); } else { window.getSelection().removeAllRanges(); } @@ -418,8 +472,11 @@ _dragStarted: function () { if (rootEl && dragEl) { + var options = this.options; + // Apply effect - _toggleClass(dragEl, this.options.ghostClass, true); + _toggleClass(dragEl, options.ghostClass, true); + _toggleClass(dragEl, options.dragClass, false); Sortable.active = this; @@ -443,12 +500,11 @@ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY), parent = target, - groupName = ' ' + this.options.group.name + '', i = touchDragOverListeners.length; if (parent) { do { - if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) { + if (parent[expando]) { while (i--) { touchDragOverListeners[i]({ clientX: touchEvt.clientX, @@ -478,9 +534,10 @@ if (tapEvt) { var options = this.options, fallbackTolerance = options.fallbackTolerance, + fallbackOffset = options.fallbackOffset, touch = evt.touches ? evt.touches[0] : evt, - dx = touch.clientX - tapEvt.clientX, - dy = touch.clientY - tapEvt.clientY, + dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, + dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; // only set the status to dragging, when we are actually dragging @@ -520,6 +577,7 @@ _toggleClass(ghostEl, options.ghostClass, false); _toggleClass(ghostEl, options.fallbackClass, true); + _toggleClass(ghostEl, options.dragClass, true); _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); @@ -545,13 +603,15 @@ this._offUpEvents(); - if (activeGroup.pull == 'clone') { - cloneEl = dragEl.cloneNode(true); + if (activeGroup.checkPull(this, this, dragEl, evt) == 'clone') { + cloneEl = _clone(dragEl); _css(cloneEl, 'display', 'none'); rootEl.insertBefore(cloneEl, dragEl); _dispatchEvent(this, rootEl, 'clone', dragEl); } + _toggleClass(dragEl, options.dragClass, true); + if (useFallback) { if (useFallback === 'touch') { // Bind touch events @@ -581,10 +641,11 @@ var el = this.el, target, dragRect, + targetRect, revert, options = this.options, group = options.group, - groupPut = group.put, + activeSortable = Sortable.active, isOwner = (activeGroup === group), canSort = options.sort; @@ -598,9 +659,9 @@ if (activeGroup && !options.disabled && (isOwner ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list - : activeGroup.pull && groupPut && ( - (activeGroup.name === group.name) || // by Name - (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array + : ( + putSortable === this || + activeGroup.checkPull(this, activeSortable, dragEl, evt) && group.checkPut(this, activeSortable, dragEl, evt) ) ) && (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback @@ -614,6 +675,7 @@ target = _closest(evt.target, options.draggable, el); dragRect = dragEl.getBoundingClientRect(); + putSortable = this; if (revert) { _cloneHide(true); @@ -633,7 +695,6 @@ if ((el.children.length === 0) || (el.children[0] === ghostEl) || (el === evt.target) && (target = _ghostIsLast(el, evt)) ) { - if (target) { if (target.animated) { return; @@ -644,7 +705,7 @@ _cloneHide(isOwner); - if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) { + if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { if (!dragEl.contains(el)) { el.appendChild(dragEl); parentEl = el; // actualization @@ -661,9 +722,9 @@ lastParentCSS = _css(target.parentNode); } + targetRect = target.getBoundingClientRect(); - var targetRect = target.getBoundingClientRect(), - width = targetRect.right - targetRect.left, + var width = targetRect.right - targetRect.left, height = targetRect.bottom - targetRect.top, floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display) || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), @@ -671,7 +732,7 @@ isLong = (target.offsetHeight > dragEl.offsetHeight), halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, nextSibling = target.nextElementSibling, - moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect), + moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt), after ; @@ -784,6 +845,7 @@ } _disableDraggable(dragEl); + dragEl.style['will-change'] = ''; // Remove class's _toggleClass(dragEl, this.options.ghostClass, false); @@ -793,15 +855,16 @@ newIndex = _index(dragEl, options.draggable); if (newIndex >= 0) { - // drag from one list and drop into another - _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex); - _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); // Add event _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex); // Remove event _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex); + + // drag from one list and drop into another + _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex); + _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); } } else { @@ -821,7 +884,8 @@ } if (Sortable.active) { - if (newIndex === null || newIndex === -1) { + /* jshint eqnull:true */ + if (newIndex == null || newIndex === -1) { newIndex = oldIndex; } @@ -837,7 +901,7 @@ this._nulling(); }, - _nulling: function () { + _nulling: function() { rootEl = dragEl = parentEl = @@ -857,6 +921,7 @@ lastEl = lastCSS = + putSortable = activeGroup = Sortable.active = null; }, @@ -1011,14 +1076,21 @@ if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { return el; } - } - while (el !== ctx && (el = el.parentNode)); + /* jshint boss:true */ + } while (el = _getParentOrHost(el)); } return null; } + function _getParentOrHost(el) { + var parent = el.host; + + return (parent && parent.nodeType) ? parent : el.parentNode; + } + + function _globalDragOver(/**Event*/evt) { if (evt.dataTransfer) { evt.dataTransfer.dropEffect = 'move'; @@ -1094,8 +1166,10 @@ function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) { + sortable = (sortable || rootEl[expando]); + var evt = document.createEvent('Event'), - options = (sortable || rootEl[expando]).options, + options = sortable.options, onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); evt.initEvent(name, true, true); @@ -1116,7 +1190,7 @@ } - function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) { + function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt) { var evt, sortable = fromEl[expando], onMoveFn = sortable.options.onMove, @@ -1135,7 +1209,7 @@ fromEl.dispatchEvent(evt); if (onMoveFn) { - retVal = onMoveFn.call(sortable, evt); + retVal = onMoveFn.call(sortable, evt, originalEvt); } return retVal; @@ -1155,9 +1229,14 @@ /** @returns {HTMLElement|false} */ function _ghostIsLast(el, evt) { var lastEl = el.lastElementChild, - rect = lastEl.getBoundingClientRect(); - - return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta + rect = lastEl.getBoundingClientRect(); + + // 5 — min delta + // abs — нельзя добавлять, а то глюки при наведении сверху + return ( + (evt.clientY - (rect.top + rect.height) > 5) || + (evt.clientX - (rect.right + rect.width) > 5) + ) && lastEl; } @@ -1251,6 +1330,15 @@ return dst; } + function _clone(el) { + return $ + ? $(el).clone(true)[0] + : (Polymer && Polymer.dom + ? Polymer.dom(el).cloneNode(true) + : el.cloneNode(true) + ); + } + // Export utils Sortable.utils = { @@ -1265,6 +1353,7 @@ throttle: _throttle, closest: _closest, toggleClass: _toggleClass, + clone: _clone, index: _index }; |